├── UI ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── res │ │ └── font │ │ │ ├── ubuntu_bold.ttf │ │ │ ├── ubuntu_light.ttf │ │ │ ├── raleway_light.ttf │ │ │ ├── ubuntu_italic.ttf │ │ │ ├── ubuntu_medium.ttf │ │ │ ├── ubuntu_regular.ttf │ │ │ └── alegreya_black_italic.ttf │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── github │ │ └── premnirmal │ │ └── tickerwidget │ │ └── ui │ │ ├── theme │ │ ├── AppShapes.kt │ │ ├── AppTheme.kt │ │ └── AppColours.kt │ │ ├── Divider.kt │ │ └── AppCard.kt ├── proguard-rules.pro └── build.gradle.kts ├── app ├── version.properties ├── src │ ├── test │ │ ├── resources │ │ │ ├── mockito-extensions │ │ │ │ └── org.mockito.plugins.MockMaker │ │ │ └── robolectric.properties │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── premnirmal │ │ │ └── ticker │ │ │ ├── mock │ │ │ └── Mocker.kt │ │ │ ├── tools │ │ │ └── Parser.kt │ │ │ ├── BaseUnitTest.kt │ │ │ └── network │ │ │ └── StocksApiTest.kt │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── font │ │ │ │ ├── ubuntu_bold.ttf │ │ │ │ ├── raleway_light.ttf │ │ │ │ ├── ubuntu_italic.ttf │ │ │ │ ├── ubuntu_light.ttf │ │ │ │ ├── ubuntu_medium.ttf │ │ │ │ ├── ubuntu_regular.ttf │ │ │ │ └── alegreya_black_italic.ttf │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_splash.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_splash.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-nodpi │ │ │ │ ├── ic_splash.png │ │ │ │ └── splash_bg_tile.png │ │ │ ├── drawable │ │ │ │ ├── widget_preview.png │ │ │ │ ├── transparent_widget_bg.xml │ │ │ │ ├── bg_splash.xml │ │ │ │ ├── app_widget_background.xml │ │ │ │ ├── ic_remove.xml │ │ │ │ ├── ic_trending_up.xml │ │ │ │ ├── ic_home.xml │ │ │ │ ├── ic_trending_down.xml │ │ │ │ ├── ic_done.xml │ │ │ │ ├── app_widget_background_dark.xml │ │ │ │ ├── dark_widget_bg.xml │ │ │ │ ├── ic_back.xml │ │ │ │ ├── ic_widget.xml │ │ │ │ ├── translucent_widget_bg.xml │ │ │ │ ├── light_widget_bg.xml │ │ │ │ ├── ic_arrow_down.xml │ │ │ │ ├── ic_add.xml │ │ │ │ ├── ic_remove_circle.xml │ │ │ │ ├── ic_add_circle.xml │ │ │ │ ├── ic_close.xml │ │ │ │ ├── ic_enlarge.xml │ │ │ │ ├── ic_money.xml │ │ │ │ ├── ic_add_to_list.xml │ │ │ │ ├── ic_more.xml │ │ │ │ ├── ic_search.xml │ │ │ │ ├── ic_edit.xml │ │ │ │ ├── ic_news.xml │ │ │ │ ├── ic_refresh.xml │ │ │ │ ├── ic_design.xml │ │ │ │ ├── ic_timeline.xml │ │ │ │ ├── ic_settings.xml │ │ │ │ └── ic_launcher_monochrome.xml │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_splash.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_splash.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_splash.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-night-nodpi │ │ │ │ └── splash_bg_tile.png │ │ │ ├── xml │ │ │ │ ├── provider_paths.xml │ │ │ │ └── widget.xml │ │ │ ├── values-v31 │ │ │ │ ├── color_palette.xml │ │ │ │ ├── themes.xml │ │ │ │ ├── styles.xml │ │ │ │ └── widget_themes.xml │ │ │ ├── values-night-v31 │ │ │ │ └── color_palette.xml │ │ │ ├── values-en-rGB │ │ │ │ └── strings.xml │ │ │ ├── values-night │ │ │ │ ├── widget_colors.xml │ │ │ │ ├── color_palette.xml │ │ │ │ └── base_themes.xml │ │ │ ├── anim │ │ │ │ ├── fade_in_fast.xml │ │ │ │ └── fade_out_fast.xml │ │ │ ├── values-v27 │ │ │ │ └── themes.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ │ ├── layout │ │ │ │ ├── loadview.xml │ │ │ │ ├── widget_empty_view.xml │ │ │ │ ├── initial_widget_layout.xml │ │ │ │ ├── text_marker_layout.xml │ │ │ │ ├── widget_3x1.xml │ │ │ │ ├── widget_4x1.xml │ │ │ │ ├── widget_5x1.xml │ │ │ │ ├── widget_1x1.xml │ │ │ │ ├── widget_2x1.xml │ │ │ │ ├── stockview3.xml │ │ │ │ ├── widget_header.xml │ │ │ │ ├── stockview2.xml │ │ │ │ └── stockview.xml │ │ │ ├── drawable-night │ │ │ │ └── app_widget_background.xml │ │ │ ├── values-v26 │ │ │ │ └── themes.xml │ │ │ ├── values │ │ │ │ ├── attrs.xml │ │ │ │ ├── widget_colors.xml │ │ │ │ ├── widget_themes.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── color_palette.xml │ │ │ │ ├── base_themes.xml │ │ │ │ ├── themes.xml │ │ │ │ ├── dimens.xml │ │ │ │ └── settings.xml │ │ │ ├── values-es │ │ │ │ └── settings.xml │ │ │ ├── values-fr │ │ │ │ └── settings.xml │ │ │ ├── values-de │ │ │ │ └── settings.xml │ │ │ ├── values-ru │ │ │ │ └── settings.xml │ │ │ ├── values-pt-rBR │ │ │ │ └── settings.xml │ │ │ └── values-it │ │ │ │ └── settings.xml │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── premnirmal │ │ │ └── ticker │ │ │ ├── home │ │ │ ├── HomeEvent.kt │ │ │ ├── IAppReviewManager.kt │ │ │ ├── CollapsingTopBarScrollConnection.kt │ │ │ └── TotalHoldingsPopup.kt │ │ │ ├── widget │ │ │ ├── IWidgetData.kt │ │ │ ├── RemoteStockProviderService.kt │ │ │ ├── WidgetsViewModel.kt │ │ │ ├── RefreshReceiver.kt │ │ │ └── WidgetClickReceiver.kt │ │ │ ├── model │ │ │ ├── FetchException.kt │ │ │ ├── FetchResult.kt │ │ │ └── RefreshWorker.kt │ │ │ ├── network │ │ │ ├── ApeWisdom.kt │ │ │ ├── GithubApi.kt │ │ │ ├── SuggestionApi.kt │ │ │ ├── data │ │ │ │ ├── NewsRssFeed.kt │ │ │ │ ├── PriceFormat.kt │ │ │ │ ├── Properties.kt │ │ │ │ ├── Trending.kt │ │ │ │ ├── DataPoint.kt │ │ │ │ ├── SuggestionsNet.kt │ │ │ │ ├── Suggestion.kt │ │ │ │ ├── RepoCommit.kt │ │ │ │ ├── Position.kt │ │ │ │ └── HistoricalData.kt │ │ │ ├── ChartApi.kt │ │ │ ├── YahooFinanceCookies.kt │ │ │ ├── NewsApis.kt │ │ │ ├── YahooFinance.kt │ │ │ ├── CrumbInterceptor.kt │ │ │ ├── JsoupConverterFactory.kt │ │ │ └── CommitsProvider.kt │ │ │ ├── news │ │ │ ├── NewsFeedItem.kt │ │ │ └── NewsFeedViewModel.kt │ │ │ ├── repo │ │ │ ├── data │ │ │ │ ├── HoldingRow.kt │ │ │ │ ├── QuoteWithHoldings.kt │ │ │ │ ├── PropertiesRow.kt │ │ │ │ └── QuoteRow.kt │ │ │ └── QuotesDB.kt │ │ │ ├── components │ │ │ ├── Injector.kt │ │ │ ├── AppClock.kt │ │ │ └── AppComponent.kt │ │ │ ├── navigation │ │ │ ├── NavigationViewModel.kt │ │ │ ├── ScrollToTop.kt │ │ │ ├── NavigationHelpers.kt │ │ │ └── RootGraph.kt │ │ │ ├── ui │ │ │ ├── AppTextField.kt │ │ │ ├── ThemeViewModel.kt │ │ │ ├── TextMarkerView.kt │ │ │ ├── TopAppBar.kt │ │ │ ├── CollectBottomSheetMessage.kt │ │ │ ├── WindowStateUtils.kt │ │ │ ├── UIStates.kt │ │ │ ├── AxisFormatters.kt │ │ │ ├── AppMessaging.kt │ │ │ ├── ModalBottomSheetWithMessage.kt │ │ │ └── LinkText.kt │ │ │ ├── UpdateReceiver.kt │ │ │ ├── notifications │ │ │ └── DailySummaryNotificationReceiver.kt │ │ │ ├── portfolio │ │ │ ├── NotesViewModel.kt │ │ │ ├── DisplaynameViewModel.kt │ │ │ ├── AlertsViewModel.kt │ │ │ ├── DecimalFormatter.kt │ │ │ ├── AddPositionViewModel.kt │ │ │ └── search │ │ │ │ └── SuggestionItem.kt │ │ │ ├── CustomTabs.kt │ │ │ ├── StocksApp.kt │ │ │ ├── analytics │ │ │ └── Analytics.kt │ │ │ ├── detail │ │ │ └── QuoteDetailCard.kt │ │ │ └── settings │ │ │ └── ExportTasks.kt │ ├── prod │ │ ├── res │ │ │ └── values │ │ │ │ └── app_name.xml │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── premnirmal │ │ │ └── ticker │ │ │ ├── components │ │ │ └── LoggingTree.kt │ │ │ ├── home │ │ │ └── AppReviewManager.kt │ │ │ └── analytics │ │ │ └── AnalyticsImpl.kt │ ├── dev │ │ ├── res │ │ │ └── values │ │ │ │ └── app_name.xml │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── premnirmal │ │ │ └── ticker │ │ │ ├── components │ │ │ └── LoggingTree.kt │ │ │ ├── analytics │ │ │ └── AnalyticsImpl.kt │ │ │ └── home │ │ │ └── AppReviewManager.kt │ └── purefoss │ │ ├── res │ │ └── values │ │ │ └── app_name.xml │ │ └── java │ │ └── com │ │ └── github │ │ └── premnirmal │ │ └── ticker │ │ ├── components │ │ └── LoggingTree.kt │ │ ├── home │ │ └── AppReviewManager.kt │ │ └── analytics │ │ └── AnalyticsImpl.kt └── updateVersionPropertiesFile.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── graphics └── stocks_widget_512x512.png ├── settings.gradle.kts ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── unit-tests.yml │ ├── version-code.yml │ └── build.yml ├── CONTRIBUTING.md ├── gradle.properties └── README.md /UI/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /UI/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/version.properties: -------------------------------------------------------------------------------- 1 | versionName=4.0.035 2 | versionCode=400000035 -------------------------------------------------------------------------------- /app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /app/src/test/resources/robolectric.properties: -------------------------------------------------------------------------------- 1 | sdk=28 2 | constants=com.github.premnirmal.tickerwidget.BuildConfig -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /graphics/stocks_widget_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/graphics/stocks_widget_512x512.png -------------------------------------------------------------------------------- /UI/src/main/res/font/ubuntu_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/UI/src/main/res/font/ubuntu_bold.ttf -------------------------------------------------------------------------------- /UI/src/main/res/font/ubuntu_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/UI/src/main/res/font/ubuntu_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/ubuntu_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/font/ubuntu_bold.ttf -------------------------------------------------------------------------------- /UI/src/main/res/font/raleway_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/UI/src/main/res/font/raleway_light.ttf -------------------------------------------------------------------------------- /UI/src/main/res/font/ubuntu_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/UI/src/main/res/font/ubuntu_italic.ttf -------------------------------------------------------------------------------- /UI/src/main/res/font/ubuntu_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/UI/src/main/res/font/ubuntu_medium.ttf -------------------------------------------------------------------------------- /UI/src/main/res/font/ubuntu_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/UI/src/main/res/font/ubuntu_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/raleway_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/font/raleway_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/ubuntu_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/font/ubuntu_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/ubuntu_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/font/ubuntu_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/ubuntu_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/font/ubuntu_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/ubuntu_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/font/ubuntu_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-hdpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-mdpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/drawable-nodpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/drawable/widget_preview.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-xhdpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_splash.png -------------------------------------------------------------------------------- /UI/src/main/res/font/alegreya_black_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/UI/src/main/res/font/alegreya_black_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/alegreya_black_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/font/alegreya_black_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/splash_bg_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/drawable-nodpi/splash_bg_tile.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /UI/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-night-nodpi/splash_bg_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/drawable-night-nodpi/splash_bg_tile.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premnirmal/StockTicker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/prod/res/values/app_name.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Stocks Widget 4 | 5 | -------------------------------------------------------------------------------- /app/src/dev/res/values/app_name.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dev Stocks Widget 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/home/HomeEvent.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.home 2 | 3 | sealed class HomeEvent { 4 | object PromptRate : HomeEvent() 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-v31/color_palette.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_accent1_700 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-night-v31/color_palette.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_accent1_400 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/widget/IWidgetData.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.widget 2 | 3 | interface IWidgetData { 4 | val widgetId: Int 5 | val widgetName: String 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-v31/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-v27/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 9 | 9 | 10 | -------------------------------------------------------------------------------- /UI/src/main/java/com/github/premnirmal/tickerwidget/ui/Divider.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import androidx.compose.material3.HorizontalDivider 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.unit.dp 7 | 8 | @Composable 9 | fun Divider( 10 | modifier: Modifier = Modifier 11 | ) { 12 | HorizontalDivider( 13 | modifier = modifier, 14 | thickness = 0.2.dp 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/SuggestionApi.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network 2 | 3 | import com.github.premnirmal.ticker.network.data.SuggestionsNet 4 | import retrofit2.http.GET 5 | import retrofit2.http.Query 6 | 7 | /** 8 | * Created by premnirmal on 3/3/16. 9 | */ 10 | interface SuggestionApi { 11 | 12 | @GET("search?quotesCount=20&newsCount=0&listsCount=0&enableFuzzyQuery=false") 13 | suspend fun getSuggestions(@Query("q") query: String): SuggestionsNet 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/repo/data/HoldingRow.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.repo.data 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity 8 | data class HoldingRow( 9 | @PrimaryKey(autoGenerate = true) var id: Long? = null, 10 | @ColumnInfo(name = "quote_symbol") val quoteSymbol: String, 11 | @ColumnInfo(name = "shares") val shares: Float = 0.0f, 12 | @ColumnInfo(name = "price") val price: Float = 0.0f 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_remove_circle.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/NewsRssFeed.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import org.simpleframework.xml.ElementList 4 | import org.simpleframework.xml.Path 5 | import org.simpleframework.xml.Root 6 | 7 | @Root(name = "rss", strict = false) 8 | class NewsRssFeed { 9 | @get:ElementList(name = "item", inline = true) 10 | @get:Path("channel") 11 | @set:ElementList(name = "item", inline = true) 12 | @set:Path("channel") 13 | var articleList: List? = null 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/xml/widget.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/components/Injector.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.components 2 | 3 | import com.github.premnirmal.ticker.StocksApp 4 | import dagger.hilt.EntryPoints 5 | 6 | /** 7 | * Created by premnirmal on 2/26/16. 8 | */ 9 | object Injector { 10 | 11 | private lateinit var app: StocksApp 12 | 13 | fun init(app: StocksApp) { 14 | this.app = app 15 | } 16 | 17 | fun appComponent(): AppEntryPoint { 18 | return EntryPoints.get(app, AppEntryPoint::class.java) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_circle.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/dev/java/com/github/premnirmal/ticker/home/AppReviewManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.home 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import com.github.premnirmal.ticker.showDialog 6 | import dagger.hilt.android.qualifiers.ApplicationContext 7 | import timber.log.Timber 8 | 9 | class AppReviewManager(@ApplicationContext context: Context) : IAppReviewManager { 10 | override fun launchReviewFlow(activity: Activity) { 11 | activity.showDialog("Launching app review flow triggered on dev build :)") 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/ChartApi.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network 2 | 3 | import com.github.premnirmal.ticker.network.data.HistoricalDataResult 4 | import retrofit2.http.GET 5 | import retrofit2.http.Path 6 | import retrofit2.http.Query 7 | 8 | interface ChartApi { 9 | 10 | @GET("chart/{symbol}") 11 | suspend fun fetchChartData( 12 | @Path("symbol") symbol: String, 13 | @Query(value = "interval") interval: String, 14 | @Query(value = "range") range: String 15 | ): HistoricalDataResult 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/repo/data/QuoteWithHoldings.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.repo.data 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | data class QuoteWithHoldings( 7 | @Embedded 8 | val quote: QuoteRow, 9 | @Relation( 10 | parentColumn = "symbol", 11 | entityColumn = "quote_symbol" 12 | ) 13 | val holdings: List, 14 | @Relation( 15 | parentColumn = "symbol", 16 | entityColumn = "properties_quote_symbol" 17 | ) 18 | val properties: PropertiesRow? 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_enlarge.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_money.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/PriceFormat.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import com.github.premnirmal.ticker.AppPreferences 4 | 5 | class PriceFormat( 6 | val currencyCode: String, 7 | val symbol: String, 8 | val prefix: Boolean = true 9 | ) { 10 | fun format(price: Float): String { 11 | val priceString = AppPreferences.SELECTED_DECIMAL_FORMAT.format(price) 12 | return if (prefix) { 13 | "$symbol$priceString" 14 | } else { 15 | "$priceString$symbol" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/color_palette.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #50aaaaaa 4 | #aaaa 5 | #aaaa 6 | #1d1d1d 7 | #95000000 8 | 9 | #ff3232 10 | #ff6666 11 | #ccff66 12 | #009900 13 | #FFFAF9F6 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/repo/QuotesDB.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.repo 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.github.premnirmal.ticker.repo.data.HoldingRow 6 | import com.github.premnirmal.ticker.repo.data.PropertiesRow 7 | import com.github.premnirmal.ticker.repo.data.QuoteRow 8 | 9 | @Database( 10 | entities = [QuoteRow::class, HoldingRow::class, PropertiesRow::class], 11 | version = 8, 12 | exportSchema = true 13 | ) 14 | abstract class QuotesDB : RoomDatabase() { 15 | abstract fun quoteDao(): QuoteDao 16 | } 17 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/premnirmal/ticker/mock/Mocker.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.mock 2 | 3 | import org.mockito.Mockito.mock 4 | import java.util.HashMap 5 | import kotlin.reflect.KClass 6 | 7 | object Mocker { 8 | 9 | private val mocks = HashMap, Any>() 10 | 11 | fun provide(clazz: KClass): T { 12 | if (!mocks.containsKey(clazz)) { 13 | val mock = mock(clazz.java) 14 | mocks[clazz] = mock!! 15 | } 16 | return mocks[clazz] as T 17 | } 18 | 19 | fun clearMocks() { 20 | mocks.clear() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_to_list.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 11 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/initial_widget_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/Properties.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.Serializable 6 | 7 | @Parcelize 8 | @Serializable 9 | data class Properties( 10 | val symbol: String, 11 | var notes: String = "", 12 | var displayname: String = "", 13 | var alertAbove: Float = 0.0f, 14 | var alertBelow: Float = 0.0f, 15 | var id: Long? = null 16 | ) : Parcelable { 17 | fun isEmpty(): Boolean = this.notes.isNotEmpty() || this.alertAbove > 0.0f || this.alertBelow > 0.0f 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/widget_colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #aaa 4 | #1b1b1b 5 | #1b1b1b 6 | #FFFAFF 7 | #FFFAFF 8 | #009900 9 | #009900 10 | #ccff66 11 | #ff6666 12 | #faf9f6 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/widget/RemoteStockProviderService.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.widget 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.content.Intent 5 | import android.widget.RemoteViewsService 6 | 7 | /** 8 | * Created by premnirmal on 2/27/16. 9 | */ 10 | class RemoteStockProviderService : RemoteViewsService() { 11 | 12 | override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { 13 | val appWidgetId = 14 | intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) 15 | return RemoteStockViewAdapter(appWidgetId) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_news.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/repo/data/PropertiesRow.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.repo.data 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity 8 | data class PropertiesRow( 9 | @PrimaryKey(autoGenerate = true) var id: Long? = null, 10 | @ColumnInfo(name = "properties_quote_symbol") val quoteSymbol: String, 11 | @ColumnInfo(name = "notes") val notes: String = "", 12 | @ColumnInfo(name = "displayname") val displayname: String = "", 13 | @ColumnInfo(name = "alert_above") val alertAbove: Float = 0.0f, 14 | @ColumnInfo(name = "alert_below") val alertBelow: Float = 0.0f 15 | ) 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /app/src/main/res/values/widget_themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/model/FetchResult.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.model 2 | 3 | data class FetchResult(private val _data: T? = null, private var _error: Throwable? = null) { 4 | 5 | companion object { 6 | inline fun success(data: T) = FetchResult(_data = data) 7 | inline fun failure(error: Throwable) = FetchResult(_error = error) 8 | } 9 | 10 | val wasSuccessful: Boolean 11 | get() = _data != null 12 | 13 | val hasError: Boolean 14 | get() = _error != null 15 | 16 | val data: T 17 | get() = _data!! 18 | 19 | val dataSafe: T? 20 | get() = _data 21 | 22 | val error: Throwable 23 | get() = _error!! 24 | } 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to StockTicker 2 | 3 | If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request. 4 | 5 | When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. 6 | 7 | Use detekt to make sure your code follows the Kotlin style guide. You can run it with the command: 8 | 9 | ```bash 10 | ./gradlew [detekt](https://detekt.dev/) --auto-correct 11 | ``` 12 | 13 | ## License 14 | 15 | By contributing your code, you agree to license your contribution under the terms of the GPL license: https://github.com/premnirmal/StockTicker/blob/master/LICENSE.txt 16 | All files are released with the GNU general public license (GPL). -------------------------------------------------------------------------------- /app/src/main/res/layout/text_marker_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/navigation/NavigationViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.navigation 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | import kotlinx.coroutines.flow.filter 8 | import kotlinx.coroutines.launch 9 | 10 | class NavigationViewModel : ViewModel() { 11 | private val _scrollToTopAction = MutableSharedFlow() 12 | 13 | fun actionForRoute(route: HomeRoute): Flow = _scrollToTopAction.filter { it == route } 14 | 15 | fun scrollToTop(route: HomeRoute) { 16 | viewModelScope.launch { 17 | _scrollToTopAction.emit(route) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/base_themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/components/AppClock.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.components 2 | 3 | import android.os.SystemClock 4 | import java.time.LocalDateTime 5 | import java.time.ZonedDateTime 6 | 7 | interface AppClock { 8 | fun todayZoned(): ZonedDateTime 9 | fun todayLocal(): LocalDateTime 10 | fun currentTimeMillis(): Long 11 | fun elapsedRealtime(): Long 12 | 13 | object AppClockImpl : AppClock { 14 | 15 | override fun todayZoned(): ZonedDateTime = ZonedDateTime.now() 16 | 17 | override fun todayLocal(): LocalDateTime = LocalDateTime.now() 18 | 19 | override fun currentTimeMillis(): Long = System.currentTimeMillis() 20 | 21 | override fun elapsedRealtime(): Long = SystemClock.elapsedRealtime() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/YahooFinanceCookies.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network 2 | 3 | import okhttp3.Cookie 4 | import okhttp3.CookieJar 5 | import okhttp3.HttpUrl 6 | import java.util.concurrent.ConcurrentHashMap 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | class YahooFinanceCookies @Inject constructor() : CookieJar { 12 | 13 | private var _cookies = ConcurrentHashMap() 14 | 15 | override fun saveFromResponse(url: HttpUrl, cookies: List) { 16 | cookies.forEach { cookie -> 17 | _cookies[cookie.name] = cookie 18 | } 19 | } 20 | 21 | override fun loadForRequest(url: HttpUrl): List { 22 | return _cookies.values.toList() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /UI/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 -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/Trending.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class TrendingResult( 8 | @SerialName("count") val count: Int, 9 | @SerialName("pages") val pages: Int, 10 | @SerialName("current_page") val currentPage: Int, 11 | @SerialName("results") val results: List 12 | ) 13 | 14 | @Serializable 15 | data class Trending( 16 | @SerialName("rank") val rank: Int?, 17 | @SerialName("mentions") val mentions: Int?, 18 | @SerialName("mentions_24h_ago") val mentions24hAgo: Int?, 19 | @SerialName("upvotes") val upvotes: Int?, 20 | @SerialName("ticker") val ticker: String, 21 | @SerialName("name") val name: String? 22 | ) 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_design.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 11 | 17 | 21 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | push: 8 | branches: 9 | - 'master' 10 | 11 | jobs: 12 | test: 13 | name: Run Unit Tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v4 21 | with: 22 | java-version: 17 23 | distribution: 'temurin' 24 | - name: Unit tests 25 | run: ./gradlew testDevDebug -PdisablePreDex --no-daemon 26 | - name: Archive test results 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: test-results 30 | path: | 31 | app/build/test-results/testDevDebugUnitTest/ 32 | app/build/reports/tests 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_3x1.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_4x1.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_5x1.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/AppTextField.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.TextFieldColors 6 | import androidx.compose.material3.TextFieldDefaults 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.Color 9 | 10 | val AppTextFieldDefaultColors: TextFieldColors 11 | @Composable get() = TextFieldDefaults.colors().copy( 12 | focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, 13 | unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, 14 | focusedIndicatorColor = Color.Transparent, 15 | disabledIndicatorColor = Color.Transparent, 16 | unfocusedIndicatorColor = Color.Transparent, 17 | ) 18 | 19 | val AppTextFieldShape = RoundedCornerShape(30) -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_1x1.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/widget/WidgetsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.widget 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.github.premnirmal.ticker.model.StocksProvider 5 | import com.github.premnirmal.ticker.model.StocksProvider.FetchState 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.StateFlow 9 | import javax.inject.Inject 10 | 11 | @HiltViewModel 12 | class WidgetsViewModel @Inject constructor( 13 | private val stocksProvider: StocksProvider, 14 | private val widgetDataProvider: WidgetDataProvider 15 | ) : ViewModel() { 16 | 17 | val widgetDataList: Flow> 18 | get() = widgetDataProvider.widgetData 19 | 20 | val fetchState: StateFlow 21 | get() = stocksProvider.fetchState 22 | 23 | fun refreshWidgets() { 24 | widgetDataProvider.refreshWidgetDataList() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/prod/java/com/github/premnirmal/ticker/components/LoggingTree.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.components 2 | 3 | import android.util.Log 4 | import com.github.premnirmal.tickerwidget.BuildConfig 5 | import com.google.firebase.crashlytics.FirebaseCrashlytics 6 | import timber.log.Timber 7 | 8 | /** 9 | * Created by premnirmal on 2/28/16. 10 | */ 11 | class LoggingTree : Timber.Tree() { 12 | 13 | private val crashlytics: FirebaseCrashlytics = FirebaseCrashlytics.getInstance().apply { 14 | setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG) 15 | } 16 | 17 | override fun log( 18 | priority: Int, 19 | tag: String?, 20 | message: String, 21 | t: Throwable? 22 | ) { 23 | if (priority == Log.VERBOSE || priority == Log.DEBUG) { 24 | return 25 | } 26 | Log.println(priority, tag, message) 27 | crashlytics.log(message) 28 | if (t != null) { 29 | Log.e(tag, t.message, t) 30 | crashlytics.recordException(t) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/DataPoint.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import com.github.mikephil.charting.data.CandleEntry 4 | import kotlinx.parcelize.Parcelize 5 | import java.io.Serializable 6 | import java.time.Instant 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.ZoneId 10 | 11 | @Parcelize 12 | class DataPoint( 13 | val xVal: Float, 14 | val shadowH: Float, 15 | val shadowL: Float, 16 | val openVal: Float, 17 | val closeVal: Float 18 | ) : CandleEntry(xVal, shadowH, shadowL, openVal, closeVal), Serializable, Comparable { 19 | 20 | fun getDate(): LocalDate = LocalDateTime.ofInstant( 21 | Instant.ofEpochSecond(x.toLong()), 22 | ZoneId.systemDefault() 23 | ).toLocalDate() 24 | 25 | override fun compareTo(other: DataPoint): Int = x.compareTo(other.x) 26 | 27 | companion object { 28 | private const val serialVersionUID = 42L 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | - App Version [e.g. 3.9.123] **Please make sure you are not on an outdated app version before you file an issue, otherwise your issue will be closed. You can check the latest version in the releases part of the repo**. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Smartphone (please complete the following information):** 28 | - Device: [e.g. iPhone6] 29 | - OS: [e.g. iOS8.1] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/NewsApis.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network 2 | 3 | import com.github.premnirmal.ticker.network.data.NewsRssFeed 4 | import org.jsoup.nodes.Document 5 | import retrofit2.Response 6 | import retrofit2.http.GET 7 | import retrofit2.http.Query 8 | 9 | interface GoogleNewsApi { 10 | 11 | /** 12 | * Retrieves the recent news feed given the query. 13 | * 14 | * @param query the query String 15 | * @return the news articles for the given query. 16 | */ 17 | @GET("rss/search/") 18 | suspend fun getNewsFeed(@Query(value = "q") query: String): NewsRssFeed 19 | 20 | @GET("news/rss/headlines/section/topic/BUSINESS") 21 | suspend fun getBusinessNews(): NewsRssFeed 22 | } 23 | 24 | interface YahooFinanceNewsApi { 25 | @GET("rssindex") 26 | suspend fun getNewsFeed(): NewsRssFeed 27 | } 28 | 29 | interface YahooFinanceMostActive { 30 | @GET("most-active") 31 | suspend fun getMostActive(): Response 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/ThemeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.github.premnirmal.ticker.AppPreferences 5 | import com.github.premnirmal.tickerwidget.ui.theme.SelectedTheme 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.map 9 | import javax.inject.Inject 10 | 11 | @HiltViewModel 12 | class ThemeViewModel @Inject constructor( 13 | private val appPreferences: AppPreferences 14 | ) : ViewModel() { 15 | 16 | val themePref: Flow 17 | get() = appPreferences.themePrefFlow.map { pref -> 18 | when (pref) { 19 | AppPreferences.LIGHT_THEME -> SelectedTheme.LIGHT 20 | AppPreferences.DARK_THEME -> SelectedTheme.DARK 21 | AppPreferences.FOLLOW_SYSTEM_THEME -> SelectedTheme.SYSTEM 22 | else -> SelectedTheme.SYSTEM 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_2x1.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## For more details on how to configure your build environment visit 2 | # http://www.gradle.org/docs/current/userguide/build_environment.html 3 | # 4 | # Specifies the JVM arguments used for the daemon process. 5 | # The setting is particularly useful for tweaking memory settings. 6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m 7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 8 | # 9 | # When configured, Gradle will run in incubating parallel mode. 10 | # This option should only be used with decoupled projects. More details, visit 11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 12 | # org.gradle.parallel=true 13 | #Sat May 06 12:58:51 BST 2023 14 | android.enableJetifier=true 15 | android.nonTransitiveRClass=true 16 | android.useAndroidX=true 17 | org.gradle.caching=true 18 | org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1024M" -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8 19 | android.enableR8.fullMode=false -------------------------------------------------------------------------------- /app/src/prod/java/com/github/premnirmal/ticker/home/AppReviewManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.home 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import com.google.android.play.core.review.ReviewManager 6 | import com.google.android.play.core.review.ReviewManagerFactory 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import timber.log.Timber 9 | 10 | class AppReviewManager(@ApplicationContext private val context: Context) : IAppReviewManager { 11 | 12 | private val manager: ReviewManager by lazy { 13 | ReviewManagerFactory.create(context) 14 | } 15 | 16 | override fun launchReviewFlow(activity: Activity) { 17 | manager.requestReviewFlow().addOnCompleteListener { task -> 18 | if (task.isSuccessful) { 19 | val reviewInfo = task.result 20 | manager.launchReviewFlow(activity, reviewInfo).addOnCompleteListener { 21 | Timber.i("Review left") 22 | } 23 | } else { 24 | Timber.w(task.exception, "Failed to request review") 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/UpdateReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.github.premnirmal.ticker.components.Injector 7 | import com.github.premnirmal.ticker.model.StocksProvider 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | /** 14 | * Created by premnirmal on 2/27/16. 15 | */ 16 | class UpdateReceiver : BroadcastReceiver() { 17 | 18 | @Inject internal lateinit var stocksProvider: StocksProvider 19 | 20 | @Inject internal lateinit var coroutineScope: CoroutineScope 21 | 22 | override fun onReceive( 23 | context: Context, 24 | intent: Intent 25 | ) { 26 | Injector.appComponent().inject(this) 27 | val pendingResult = goAsync() 28 | coroutineScope.launch(Dispatchers.Main) { 29 | stocksProvider.fetch() 30 | pendingResult.finish() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/widget/RefreshReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.widget 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.github.premnirmal.ticker.components.Injector 7 | import com.github.premnirmal.ticker.model.StocksProvider 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | /** 14 | * Created by premnirmal on 2/26/16. 15 | */ 16 | class RefreshReceiver : BroadcastReceiver() { 17 | 18 | @Inject internal lateinit var stocksProvider: StocksProvider 19 | 20 | @Inject internal lateinit var coroutineScope: CoroutineScope 21 | 22 | override fun onReceive( 23 | context: Context, 24 | intent: Intent 25 | ) { 26 | Injector.appComponent().inject(this) 27 | val pendingResult = goAsync() 28 | coroutineScope.launch(Dispatchers.Main) { 29 | stocksProvider.fetch() 30 | pendingResult.finish() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/notifications/DailySummaryNotificationReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.notifications 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.github.premnirmal.ticker.AppPreferences 7 | import com.github.premnirmal.ticker.components.AppClock 8 | import com.github.premnirmal.ticker.components.Injector 9 | import timber.log.Timber 10 | import javax.inject.Inject 11 | 12 | class DailySummaryNotificationReceiver : BroadcastReceiver() { 13 | 14 | @Inject lateinit var notificationsHandler: NotificationsHandler 15 | 16 | @Inject lateinit var appPreferences: AppPreferences 17 | 18 | @Inject lateinit var clock: AppClock 19 | 20 | override fun onReceive(context: Context, intent: Intent?) { 21 | Timber.d("DailySummaryNotificationReceiver onReceive") 22 | Injector.appComponent().inject(this) 23 | val today = clock.todayLocal().toLocalDate() 24 | if (appPreferences.updateDays().contains(today.dayOfWeek)) { 25 | notificationsHandler.notifyDailySummary() 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/values/color_palette.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #ffbdbdbd 5 | #98545454 6 | #be6663 7 | #fbfcfa 8 | #10000000 9 | @color/transparent 10 | #aaaa 11 | @color/transparent 12 | #2A2A2A 13 | 14 | #000000 15 | #ffffff 16 | #A6FFFFFF 17 | #00000000 18 | #98000000 19 | #66BB6A 20 | #EF5350 21 | #ff3232 22 | #e55b5b 23 | #009900 24 | #006b00 25 | #6e6e6e 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/YahooFinance.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network 2 | 3 | import com.github.premnirmal.ticker.network.data.YahooResponse 4 | import okhttp3.RequestBody 5 | import retrofit2.Response 6 | import retrofit2.http.Body 7 | import retrofit2.http.GET 8 | import retrofit2.http.POST 9 | import retrofit2.http.Query 10 | import retrofit2.http.Url 11 | 12 | interface YahooFinance { 13 | 14 | /** 15 | * Retrieves a list of stock quotes. 16 | * 17 | * @param query comma separated list of symbols. 18 | * 19 | * @return A List of quotes. 20 | */ 21 | @GET( 22 | "v7/finance/quote?format=json" 23 | ) 24 | suspend fun getStocks(@Query(value = "symbols") query: String): Response 25 | } 26 | interface YahooFinanceInitialLoad { 27 | @GET("/") 28 | suspend fun initialLoad(): Response 29 | 30 | @POST 31 | suspend fun cookieConsent(@Url url: String?, @Body body: RequestBody): Response 32 | } 33 | interface YahooFinanceCrumb { 34 | 35 | @GET( 36 | "v1/test/getcrumb" 37 | ) 38 | suspend fun getCrumb(): Response 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/res/values/base_themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/navigation/ScrollToTop.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.State 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.lifecycle.viewmodel.compose.viewModel 9 | 10 | @Composable 11 | fun rememberScrollToTopAction( 12 | route: HomeRoute, 13 | data: Any? = null, 14 | scrollToTop: suspend () -> Unit 15 | ): State { 16 | val viewModelStoreOwner = checkNotNull(LocalNavGraphViewModelStoreOwner.current) { 17 | "No ViewModelStoreOwner was provided via LocalNavGraphViewModelStoreOwner" 18 | } 19 | val navigationViewModel = viewModel(viewModelStoreOwner) 20 | val scrolling = remember { 21 | mutableStateOf(false) 22 | } 23 | LaunchedEffect(route, data) { 24 | navigationViewModel.actionForRoute(route).collect { 25 | scrolling.value = true 26 | scrollToTop() 27 | scrolling.value = false 28 | } 29 | } 30 | return scrolling 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/CrumbInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network 2 | 3 | import com.github.premnirmal.ticker.AppPreferences 4 | import okhttp3.Interceptor 5 | import okhttp3.Interceptor.Chain 6 | import okhttp3.Response 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | class CrumbInterceptor @Inject constructor( 12 | private val appPreferences: AppPreferences 13 | ) : Interceptor { 14 | 15 | override fun intercept(chain: Chain): Response { 16 | val crumb = appPreferences.getCrumb() 17 | val builder = chain.request().newBuilder() 18 | builder 19 | .removeHeader("Accept") 20 | .addHeader( 21 | "Accept", 22 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" 23 | ) 24 | if (!crumb.isNullOrEmpty()) { 25 | builder 26 | .url(chain.request().url.newBuilder().addQueryParameter("crumb", crumb).build()) 27 | } 28 | return chain.proceed( 29 | builder.build() 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_timeline.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/SuggestionsNet.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * Created by premnirmal on 3/30/17. 8 | */ 9 | @Serializable 10 | data class SuggestionsNet( 11 | @SerialName("count") var count: Int, 12 | @SerialName("quotes") var result: List? = null 13 | ) { 14 | 15 | @Serializable 16 | data class SuggestionNet( 17 | @SerialName("symbol") var symbol: String = "" 18 | ) { 19 | @SerialName("shortname") 20 | var name: String = "" 21 | 22 | @SerialName("longname") 23 | var longName: String = "" 24 | 25 | @SerialName("exchange") 26 | var exch: String = "" 27 | 28 | @SerialName("quoteType") 29 | var type: String = "" 30 | 31 | @SerialName("exchDisp") 32 | var exchDisp: String = "" 33 | 34 | @SerialName("typeDisp") 35 | var typeDisp: String = "" 36 | 37 | @SerialName("score") 38 | var score: Float? = 0f 39 | 40 | @SerialName("isYahooFinance") 41 | var isYahooFinance: Boolean = false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 11 | 23 | -------------------------------------------------------------------------------- /.github/workflows/version-code.yml: -------------------------------------------------------------------------------- 1 | name: version-code-change 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | update-version-code: 10 | name: Update version.properties 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | token: "${{ secrets.GITHUB_TOKEN }}" 18 | fetch-depth: 0 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v4 21 | with: 22 | java-version: 17 23 | distribution: 'temurin' 24 | - name: Setup Gradle 25 | uses: gradle/actions/setup-gradle@v4 26 | - name: Update version file 27 | run: ./gradlew updateVersionPropertiesFile 28 | - name: Log version.properties file 29 | run: cat app/version.properties 30 | - name: Commit version.properties changes 31 | run: | 32 | git config --local user.name "github-actions" 33 | git config --local user.email "github-actions@github.com" 34 | git status 35 | git add . 36 | git commit -am "github-actions[bot]: Updated version.properties" 37 | - name: Push version.properties changes 38 | run: git push origin master -------------------------------------------------------------------------------- /app/src/main/res/values-es/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pequeña 6 | Medio 7 | Grande 8 | 9 | 10 | 11 | Clara 12 | Oscura 13 | Seguir sistema 14 | 15 | 16 | 17 | Animado 18 | Pestañas 19 | Fijo 20 | Mi cartera 21 | 22 | 23 | 24 | Defecto 25 | Stock único en una fila 26 | 27 | 28 | 29 | Seguir sistema 30 | Transparente 31 | Translucente 32 | 33 | 34 | 35 | Seguir sistema 36 | Clara 37 | Oscura 38 | 39 | 40 | 41 | 5 minutos 42 | 15 minutos 43 | 30 minutos 44 | 45 minutos 45 | 1 hora 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Petit 6 | Moyen 7 | Grand 8 | 9 | 10 | 11 | Clair 12 | Sombre 13 | Selon le système 14 | 15 | 16 | 17 | Animé 18 | Tableau 19 | Fixe 20 | Mon portefeuille 21 | 22 | 23 | 24 | Défaut 25 | Stock unique consécutif 26 | 27 | 28 | 29 | Selon le système 30 | Transparent 31 | Translucent 32 | 33 | 34 | 35 | Selon le système 36 | Blanc 37 | Noir 38 | 39 | 40 | 41 | 5 minutes 42 | 15 minutes 43 | 30 minutes 44 | 45 minutes 45 | 1 heure 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/portfolio/NotesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.portfolio 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.github.premnirmal.ticker.model.StocksProvider 6 | import com.github.premnirmal.ticker.network.data.Properties 7 | import com.github.premnirmal.ticker.network.data.Quote 8 | import com.github.premnirmal.ticker.repo.StocksStorage 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class NotesViewModel @Inject constructor( 15 | private val stocksProvider: StocksProvider, 16 | private val stocksStorage: StocksStorage 17 | ) : ViewModel() { 18 | 19 | lateinit var symbol: String 20 | val quote: Quote? 21 | get() = stocksProvider.getStock(symbol) 22 | 23 | fun setNotes(notesText: String) { 24 | viewModelScope.launch { 25 | quote?.let { 26 | val properties = it.properties ?: Properties( 27 | symbol 28 | ) 29 | it.properties = properties 30 | properties.notes = notesText 31 | stocksStorage.saveQuoteProperties(properties) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/CustomTabs.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.net.Uri 6 | import androidx.browser.customtabs.CustomTabColorSchemeParams 7 | import androidx.browser.customtabs.CustomTabsIntent 8 | import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON 9 | import com.github.premnirmal.ticker.settings.WebViewActivity 10 | import timber.log.Timber 11 | 12 | object CustomTabs { 13 | 14 | fun openTab( 15 | context: Context, 16 | url: String, 17 | color: Int, 18 | ) { 19 | try { 20 | val customTabsIntent = CustomTabsIntent.Builder() 21 | .setShareState(SHARE_STATE_ON) 22 | .setShowTitle(true) 23 | .setDefaultColorSchemeParams( 24 | CustomTabColorSchemeParams.Builder().setToolbarColor(color).build() 25 | ) 26 | .setExitAnimations(context, android.R.anim.fade_in, android.R.anim.fade_out) 27 | .build() 28 | customTabsIntent.launchUrl(context, Uri.parse(url)) 29 | } catch (e: ActivityNotFoundException) { 30 | Timber.w(e) 31 | context.startActivity(WebViewActivity.newIntent(context, url)) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/portfolio/DisplaynameViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.portfolio 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.github.premnirmal.ticker.model.StocksProvider 6 | import com.github.premnirmal.ticker.network.data.Properties 7 | import com.github.premnirmal.ticker.network.data.Quote 8 | import com.github.premnirmal.ticker.repo.StocksStorage 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class DisplaynameViewModel @Inject constructor( 15 | private val stocksProvider: StocksProvider, 16 | private val stocksStorage: StocksStorage 17 | ) : ViewModel() { 18 | 19 | lateinit var symbol: String 20 | val quote: Quote? 21 | get() = stocksProvider.getStock(symbol) 22 | 23 | fun setDisplayname(displaynameText: String) { 24 | viewModelScope.launch { 25 | quote?.let { 26 | val properties = it.properties ?: Properties( 27 | symbol 28 | ) 29 | it.properties = properties 30 | properties.displayname = displaynameText 31 | stocksStorage.saveQuoteProperties(properties) 32 | } 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/model/RefreshWorker.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.model 2 | 3 | import android.content.Context 4 | import androidx.work.CoroutineWorker 5 | import androidx.work.WorkerParameters 6 | import com.github.premnirmal.ticker.components.Injector 7 | import com.github.premnirmal.ticker.isNetworkOnline 8 | import javax.inject.Inject 9 | 10 | class RefreshWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { 11 | 12 | companion object { 13 | const val TAG = "RefreshWorker" 14 | const val TAG_PERIODIC = "RefreshWorker_Periodic" 15 | } 16 | 17 | @Inject internal lateinit var stocksProvider: StocksProvider 18 | 19 | @Inject internal lateinit var alarmScheduler: AlarmScheduler 20 | 21 | override suspend fun doWork(): Result { 22 | return if (applicationContext.isNetworkOnline()) { 23 | Injector.appComponent().inject(this) 24 | if (!alarmScheduler.isCurrentTimeWithinScheduledUpdateTime()) { 25 | return Result.success() 26 | } 27 | val result = stocksProvider.fetch() 28 | if (result.hasError) { 29 | Result.retry() 30 | } else { 31 | Result.success() 32 | } 33 | } else { 34 | Result.retry() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/JsoupConverterFactory.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network 2 | 3 | import okhttp3.ResponseBody 4 | import org.jsoup.Jsoup 5 | import org.jsoup.nodes.Document 6 | import org.jsoup.parser.Parser 7 | import retrofit2.Converter 8 | import retrofit2.Retrofit 9 | import java.lang.reflect.Type 10 | import java.nio.charset.Charset 11 | 12 | class JsoupConverterFactory : Converter.Factory() { 13 | 14 | override fun responseBodyConverter( 15 | type: Type, 16 | annotations: Array, 17 | retrofit: Retrofit 18 | ): Converter? { 19 | return when (type) { 20 | Document::class.java -> JsoupConverter(retrofit.baseUrl().toString()) 21 | else -> null 22 | } 23 | } 24 | 25 | private class JsoupConverter(val baseUri: String) : Converter { 26 | 27 | override fun convert(value: ResponseBody): Document? { 28 | val charset = value.contentType()?.charset() ?: Charset.forName("UTF-8") 29 | val parser = when (value.contentType().toString()) { 30 | "application/xml", "text/xml" -> Parser.xmlParser() 31 | else -> Parser.htmlParser() 32 | } 33 | return Jsoup.parse(value.byteStream(), charset.name(), baseUri, parser) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/updateVersionPropertiesFile.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.ByteArrayOutputStream 2 | import java.io.FileInputStream 3 | import java.util.Locale 4 | import java.util.Properties 5 | 6 | // Used by github action in version-code.yml 7 | tasks.create("updateVersionPropertiesFile") { 8 | doLast { 9 | println("Updating version.properties file for F-droid") 10 | 11 | updateVersionPropertiesFile() 12 | } 13 | } 14 | 15 | fun updateVersionPropertiesFile() { 16 | val rootDir = projectDir.absolutePath 17 | val filePath = "$rootDir/app/version.properties" 18 | 19 | val name = getVersionName() 20 | val major = name.split(".")[0].toInt() 21 | val minor = name.split(".")[1].toInt() 22 | val patch = name.split(".")[2].toInt() 23 | val code = (major * 100000000) + (minor * 100000) + patch 24 | 25 | val output = buildString { 26 | append("versionName=$name") 27 | append("\n") 28 | append("versionCode=$code") 29 | } 30 | val file = File(filePath) 31 | file.writeText(output.toString()) 32 | } 33 | 34 | 35 | fun getVersionName(): String { 36 | return ByteArrayOutputStream().use { outputStream -> 37 | val stdout = outputStream 38 | exec { 39 | commandLine("git", "describe", "--tags", "--abbrev=0") 40 | standardOutput = stdout 41 | } 42 | stdout.toString().trim() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/portfolio/AlertsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.portfolio 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.github.premnirmal.ticker.model.StocksProvider 6 | import com.github.premnirmal.ticker.network.data.Properties 7 | import com.github.premnirmal.ticker.network.data.Quote 8 | import com.github.premnirmal.ticker.repo.StocksStorage 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class AlertsViewModel @Inject constructor( 15 | private val stocksProvider: StocksProvider, 16 | private val stocksStorage: StocksStorage 17 | ) : ViewModel() { 18 | 19 | lateinit var symbol: String 20 | val quote: Quote? 21 | get() = stocksProvider.getStock(symbol) 22 | 23 | fun setAlerts( 24 | alertAbove: Float, 25 | alertBelow: Float 26 | ) { 27 | viewModelScope.launch { 28 | quote?.let { 29 | val properties = it.properties ?: Properties( 30 | symbol 31 | ) 32 | it.properties = properties.apply { 33 | this.alertAbove = alertAbove 34 | this.alertBelow = alertBelow 35 | } 36 | stocksStorage.saveQuoteProperties(properties) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/TextMarkerView.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import android.content.Context 4 | import android.widget.TextView 5 | import com.github.mikephil.charting.components.MarkerView 6 | import com.github.mikephil.charting.data.Entry 7 | import com.github.mikephil.charting.highlight.Highlight 8 | import com.github.mikephil.charting.utils.MPPointF 9 | import com.github.premnirmal.ticker.AppPreferences 10 | import com.github.premnirmal.ticker.AppPreferences.Companion.DATE_FORMATTER 11 | import com.github.premnirmal.ticker.network.data.DataPoint 12 | import com.github.premnirmal.tickerwidget.R 13 | 14 | class TextMarkerView(context: Context) : MarkerView(context, R.layout.text_marker_layout) { 15 | 16 | private var tvContent: TextView = findViewById(R.id.tvContent) 17 | private val offsetPoint by lazy { 18 | MPPointF(-(width / 2).toFloat(), -height.toFloat()) 19 | } 20 | 21 | override fun refreshContent( 22 | e: Entry?, 23 | highlight: Highlight? 24 | ) { 25 | if (e is DataPoint) { 26 | val price = AppPreferences.DECIMAL_FORMAT.format(e.y) 27 | val date = e.getDate().format(DATE_FORMATTER) 28 | tvContent.text = "${price}\n$date" 29 | } else { 30 | tvContent.text = "" 31 | } 32 | super.refreshContent(e, highlight) 33 | } 34 | 35 | override fun getOffset(): MPPointF = offsetPoint 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/Suggestion.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import android.os.Parcelable 4 | import com.github.premnirmal.ticker.network.data.SuggestionsNet.SuggestionNet 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class Suggestion( 9 | val symbol: String = "" 10 | ) : Parcelable { 11 | var name: String = "" 12 | var exch: String = "" 13 | var type: String = "" 14 | var exchDisp: String = "" 15 | var typeDisp: String = "" 16 | 17 | fun displayString(): String { 18 | val builder = StringBuilder(symbol) 19 | if (name.isNotEmpty()) { 20 | builder.append(" - ") 21 | builder.append(name) 22 | } 23 | if (exch.isNotEmpty()) { 24 | builder.append(" (") 25 | builder.append(exch) 26 | builder.append(")") 27 | } 28 | return builder.toString() 29 | } 30 | 31 | companion object { 32 | fun fromSuggestionNet(suggestionNet: SuggestionNet): Suggestion { 33 | val suggestion = Suggestion(suggestionNet.symbol) 34 | suggestion.name = suggestionNet.name 35 | suggestion.exch = suggestionNet.exch 36 | suggestion.type = suggestionNet.type 37 | suggestion.exchDisp = suggestionNet.exchDisp 38 | suggestion.typeDisp = suggestionNet.typeDisp 39 | return suggestion 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/stockview3.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 32 | 33 | 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stocks Widget 2 | [![Build](https://github.com/premnirmal/StockTicker/workflows/Build/badge.svg)](https://github.com/premnirmal/StockTicker/actions) [![Unit tests](https://github.com/premnirmal/StockTicker/workflows/Run%20unit%20tests/badge.svg)](https://github.com/premnirmal/StockTicker/actions) 3 | 4 | 5 | Get it on Google Play 6 | 7 | 8 | Get it on F-Droid 9 | 10 | 11 | ![](https://play-lh.googleusercontent.com/R9khJ5kNzXHUjO4BxNw1cNKTx62grZ7FtLRT_F2H0BhC99iuMWDxvuGTYvyydtqE3w=h400-rw) 12 | ![](https://play-lh.googleusercontent.com/uxQfuEmietfmyq4e-xNEAXfwtkWFE9iVbJYpMtc55yKqOYTv25ViSGS1dTf6qrncXIo=h400-rw) 13 | ![](https://play-lh.googleusercontent.com/fQZFK93aeUVMr0BDNIuk8Ol9i-HC4d7GCtk01VtKr2-qcdtpmR8gO3-DJMCPbTwsCA=h400-rw) 14 | 15 | ## App features 16 | 17 | - A home screen widget that shows your stock portfolio in a resizable grid 18 | - Stocks can be sorted by dragging and dropping the list 19 | - Only performs automatic fetching of stocks during trading hours 20 | - Displays price change and summary alerts 21 | 22 | ## License 23 | 24 | GPL 25 | 26 | ### Author 27 | [Prem Nirmal](http://premnirmal.me/) 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/components/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.components 2 | 3 | import com.github.premnirmal.ticker.AppPreferences 4 | import com.github.premnirmal.ticker.UpdateReceiver 5 | import com.github.premnirmal.ticker.model.RefreshWorker 6 | import com.github.premnirmal.ticker.notifications.DailySummaryNotificationReceiver 7 | import com.github.premnirmal.ticker.widget.RefreshReceiver 8 | import com.github.premnirmal.ticker.widget.RemoteStockViewAdapter 9 | import com.github.premnirmal.ticker.widget.StockWidget 10 | import com.github.premnirmal.ticker.widget.WidgetClickReceiver 11 | import com.github.premnirmal.ticker.widget.WidgetData 12 | import dagger.hilt.EntryPoint 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.components.SingletonComponent 15 | import kotlinx.serialization.json.Json 16 | 17 | /** 18 | * Created by premnirmal on 3/3/16. 19 | */ 20 | 21 | interface LegacyComponent { 22 | fun json(): Json 23 | fun appPreferences(): AppPreferences 24 | fun inject(widget: StockWidget) 25 | fun inject(data: WidgetData) 26 | fun inject(adapter: RemoteStockViewAdapter) 27 | fun inject(receiver: WidgetClickReceiver) 28 | fun inject(receiver: RefreshReceiver) 29 | fun inject(receiver: UpdateReceiver) 30 | fun inject(receiver: DailySummaryNotificationReceiver) 31 | fun inject(refreshWorker: RefreshWorker) 32 | } 33 | 34 | @InstallIn(SingletonComponent::class) 35 | @EntryPoint 36 | interface AppEntryPoint : LegacyComponent 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/StocksApp.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker 2 | 3 | import android.app.Application 4 | import androidx.appcompat.app.AppCompatDelegate 5 | import com.github.premnirmal.ticker.analytics.Analytics 6 | import com.github.premnirmal.ticker.components.Injector 7 | import com.github.premnirmal.ticker.components.LoggingTree 8 | import com.github.premnirmal.ticker.notifications.NotificationsHandler 9 | import com.github.premnirmal.ticker.widget.WidgetDataProvider 10 | import dagger.hilt.android.HiltAndroidApp 11 | import timber.log.Timber 12 | import javax.inject.Inject 13 | 14 | /** 15 | * Created by premnirmal on 2/26/16. 16 | */ 17 | @HiltAndroidApp 18 | open class StocksApp : Application() { 19 | 20 | @Inject lateinit var analytics: Analytics 21 | 22 | @Inject lateinit var appPreferences: AppPreferences 23 | 24 | @Inject lateinit var notificationsHandler: NotificationsHandler 25 | 26 | @Inject lateinit var widgetDataProvider: WidgetDataProvider 27 | 28 | override fun onCreate() { 29 | initLogger() 30 | Injector.init(this) 31 | super.onCreate() 32 | AppCompatDelegate.setDefaultNightMode(appPreferences.nightMode) 33 | initNotificationHandler() 34 | widgetDataProvider.refreshWidgetDataList() 35 | } 36 | 37 | protected open fun initNotificationHandler() { 38 | notificationsHandler.initialize() 39 | } 40 | 41 | protected open fun initLogger() { 42 | Timber.plant(LoggingTree()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /UI/src/main/java/com/github/premnirmal/tickerwidget/ui/AppCard.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.tickerwidget.ui 2 | 3 | import androidx.compose.foundation.interaction.MutableInteractionSource 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.material3.Card 6 | import androidx.compose.material3.CardColors 7 | import androidx.compose.material3.CardDefaults 8 | import androidx.compose.material3.CardElevation 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Shape 14 | import androidx.compose.ui.unit.dp 15 | 16 | @OptIn(ExperimentalMaterial3Api::class) 17 | @Composable 18 | fun AppCard( 19 | modifier: Modifier = Modifier, 20 | enabled: Boolean = true, 21 | shape: Shape = MaterialTheme.shapes.large, 22 | colors: CardColors = CardDefaults.cardColors().copy( 23 | containerColor = MaterialTheme.colorScheme.surfaceContainerLow 24 | ), 25 | onClick: () -> Unit = {}, 26 | interactionSource: MutableInteractionSource? = null, 27 | content: @Composable ColumnScope.() -> Unit 28 | ) { 29 | Card( 30 | modifier = modifier, 31 | shape = shape, 32 | enabled = enabled, 33 | colors = colors, 34 | elevation = CardDefaults.cardElevation(1.dp), 35 | content = content, 36 | interactionSource = interactionSource, 37 | onClick = onClick 38 | ) 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/RepoCommit.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class TagComparison( 8 | @SerialName("url") val url: String, 9 | @SerialName("html_url") val html_url: String, 10 | @SerialName("commits") val commits: List, 11 | @SerialName("status") val status: String, 12 | @SerialName("ahead_by") val aheadBy: Int, 13 | @SerialName("behind_by") val behindBy: Int, 14 | @SerialName("total_commits") val totalCommits: Int 15 | ) 16 | 17 | @Serializable 18 | data class RepoCommit( 19 | @SerialName("sha") val sha: String, 20 | @SerialName("node_id") val node_id: String, 21 | @SerialName("url") val url: String, 22 | @SerialName("html_url") val html_url: String, 23 | @SerialName("commit") val commit: Commit, 24 | @SerialName("author") val author: Author 25 | ) 26 | 27 | @Serializable 28 | data class Commit( 29 | @SerialName("author") val author: Committer, 30 | @SerialName("committer") val committer: Committer, 31 | @SerialName("message") val message: String 32 | ) 33 | 34 | @Serializable 35 | data class Committer( 36 | @SerialName("name") val name: String, 37 | @SerialName("email") val email: String 38 | ) 39 | 40 | @Serializable 41 | data class Author( 42 | @SerialName("login") val login: String, 43 | @SerialName("id") val id: Long, 44 | @SerialName("type") val type: String, 45 | @SerialName("url") val url: String 46 | ) 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/analytics/Analytics.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.analytics 2 | 3 | import android.app.Activity 4 | import com.github.premnirmal.ticker.model.StocksProvider 5 | import com.github.premnirmal.ticker.widget.WidgetDataProvider 6 | import javax.inject.Inject 7 | 8 | interface Analytics { 9 | fun trackScreenView(screenName: String, activity: Activity) {} 10 | fun trackClickEvent(event: ClickEvent) {} 11 | fun trackGeneralEvent(event: GeneralEvent) {} 12 | } 13 | 14 | sealed class AnalyticsEvent(val name: String) { 15 | 16 | val properties: Map 17 | get() = _properties 18 | private val _properties = HashMap() 19 | 20 | open fun addProperty(key: String, value: String) = apply { 21 | _properties[key] = value 22 | } 23 | } 24 | 25 | class GeneralEvent(name: String) : AnalyticsEvent(name) { 26 | override fun addProperty(key: String, value: String) = apply { 27 | super.addProperty(key, value) 28 | } 29 | } 30 | 31 | class ClickEvent(name: String) : AnalyticsEvent(name) { 32 | override fun addProperty(key: String, value: String) = apply { 33 | super.addProperty(key, value) 34 | } 35 | } 36 | 37 | class GeneralProperties @Inject constructor( 38 | private val widgetDataProvider: WidgetDataProvider, 39 | private val stocksProvider: StocksProvider 40 | ) { 41 | 42 | val widgetCount: Int 43 | get() = widgetDataProvider.getAppWidgetIds().size 44 | val tickerCount: Int 45 | get() = stocksProvider.tickers.value.size 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/res/values-de/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Klein 6 | Mittel 7 | Groß 8 | 9 | 10 | 11 | Hell 12 | Dunkel 13 | Systemstandardeinstellung 14 | 15 | 16 | 17 | Animiert 18 | Tabs 19 | Fest 20 | Mein Portfolio 21 | 22 | 23 | 24 | Standard 25 | Einzeilig darstellen 26 | 27 | 28 | 29 | Systemstandardeinstellung 30 | Transparent 31 | Translucent 32 | 33 | 34 | 35 | Systemstandardeinstellung 36 | Weiß 37 | Schwarz 38 | 39 | 40 | 41 | 5 Minuten 42 | 15 Minuten 43 | 30 Minuten 44 | 45 Minuten 45 | 1 Stunde 46 | 47 | 48 | 49 | Montag 50 | Dienstag 51 | Mittwoch 52 | Donnerstag 53 | Freitag 54 | Samstag 55 | Sonntag 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/TopAppBar.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import androidx.compose.foundation.layout.RowScope 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Text 7 | import androidx.compose.material3.TopAppBar 8 | import androidx.compose.material3.TopAppBarColors 9 | import androidx.compose.material3.TopAppBarDefaults 10 | import androidx.compose.material3.TopAppBarScrollBehavior 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.text.TextStyle 14 | import androidx.compose.ui.tooling.preview.Preview 15 | 16 | @OptIn(ExperimentalMaterial3Api::class) 17 | @Composable 18 | fun TopBar( 19 | modifier: Modifier = Modifier, 20 | text: String, 21 | navigationIcon: @Composable () -> Unit = {}, 22 | actions: @Composable RowScope.() -> Unit = {}, 23 | colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), 24 | scrollBehavior: TopAppBarScrollBehavior? = null, 25 | textStyle: TextStyle = MaterialTheme.typography.headlineMedium, 26 | ) { 27 | TopAppBar( 28 | modifier = modifier, 29 | scrollBehavior = scrollBehavior, 30 | navigationIcon = navigationIcon, 31 | actions = actions, 32 | colors = colors, 33 | title = { 34 | Text(text = text, style = textStyle) 35 | } 36 | ) 37 | } 38 | 39 | @OptIn(ExperimentalMaterial3Api::class) 40 | @Preview 41 | @Composable 42 | fun TopBarPreview() { 43 | TopBar(text = "Test TopBar") 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Маленький 6 | Средний 7 | Большой 8 | 9 | 10 | 11 | Светлая 12 | Тёмная 13 | Следовать настройкам системы 14 | 15 | 16 | 17 | Анимационный 18 | Закладки 19 | Фиксированный 20 | Моё портфолио 21 | 22 | 23 | 24 | По умолчанию 25 | В одну строку 26 | 27 | 28 | 29 | Следовать настройкам системы 30 | Прозрачный 31 | Полупрозрачный 32 | 33 | 34 | 35 | Следовать настройкам системы 36 | Светлый 37 | Тёмный 38 | 39 | 40 | 41 | Каждые 5 минут 42 | Каждые 15 минут 43 | Каждые 30 минут 44 | Каждые 45 минут 45 | Каждый час 46 | 47 | 48 | 49 | Понедельник 50 | Вторник 51 | Среда 52 | Четверг 53 | Пятница 54 | Суббота 55 | Воскресенье 56 | 57 | 58 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | push: 8 | branches: 9 | - 'master' 10 | tags: 11 | - '*' 12 | 13 | jobs: 14 | build-and-release: 15 | name: Build and release 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | token: "${{ secrets.GITHUB_TOKEN }}" 21 | fetch-depth: 0 22 | - name: Set up JDK 17 23 | uses: actions/setup-java@v4 24 | with: 25 | java-version: 17 26 | distribution: 'temurin' 27 | - name: Get latest version 28 | run: | 29 | echo $(git describe --tags --abbrev=0) 30 | echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV 31 | - name: Setup Gradle 32 | uses: gradle/actions/setup-gradle@v4 33 | - name: Build bundle 34 | run: ./gradlew :app:bundleDevDebug 35 | - name: Build APK 36 | run: ./gradlew :app:assembleDevDebug 37 | - name: Upload APK and bundle 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: app 41 | path: app/build/outputs/ 42 | - name: Create release 43 | if: startsWith(github.ref, 'refs/tags/') 44 | uses: marvinpinto/action-automatic-releases@latest 45 | with: 46 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 47 | prerelease: false 48 | automatic_release_tag: "${{ env.VERSION }}" 49 | title: "${{ env.VERSION }}" 50 | files: | 51 | app/build/outputs/apk/dev/debug/app-dev-debug.apk 52 | app/build/outputs/bundle/dev/app-dev-debug.aab 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/home/CollapsingTopBarScrollConnection.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.home 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableIntStateOf 5 | import androidx.compose.runtime.saveable.Saver 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.geometry.Offset 8 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 9 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource 10 | 11 | class CollapsingTopBarScrollConnection( 12 | val appBarMaxHeight: Int, 13 | initialOffset: Int = 0 14 | ) : NestedScrollConnection { 15 | 16 | var appBarOffset by mutableIntStateOf(initialOffset) 17 | private set 18 | 19 | fun resetOffset() { 20 | appBarOffset = 0 21 | } 22 | 23 | override fun onPreScroll( 24 | available: Offset, 25 | source: NestedScrollSource 26 | ): Offset { 27 | val delta = available.y.toInt() 28 | val newOffset = appBarOffset + delta 29 | val previousOffset = appBarOffset 30 | appBarOffset = newOffset.coerceIn(-appBarMaxHeight, 0) 31 | val consumed = appBarOffset - previousOffset 32 | return Offset(0f, consumed.toFloat()) 33 | } 34 | 35 | companion object { 36 | fun saver(maxAppBarHeight: Int) = Saver( 37 | save = { it.appBarOffset }, 38 | restore = { offset -> 39 | CollapsingTopBarScrollConnection( 40 | appBarMaxHeight = maxAppBarHeight, 41 | initialOffset = offset 42 | ) 43 | } 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/detail/QuoteDetailCard.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.detail 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.unit.dp 12 | import com.github.premnirmal.ticker.news.QuoteDetailViewModel.QuoteDetail 13 | import com.github.premnirmal.ticker.ui.LocalAppMessaging 14 | import com.github.premnirmal.tickerwidget.ui.AppCard 15 | 16 | @Composable 17 | fun QuoteDetailCard( 18 | modifier: Modifier = Modifier, 19 | item: QuoteDetail 20 | ) { 21 | val appMessaging = LocalAppMessaging.current 22 | AppCard( 23 | modifier = modifier.fillMaxSize(), 24 | onClick = { 25 | appMessaging.sendBottomSheet(item.title, item.data) 26 | } 27 | ) { 28 | Column( 29 | modifier = Modifier 30 | .fillMaxSize() 31 | .padding(all = 16.dp) 32 | ) { 33 | Text( 34 | text = stringResource(item.title), 35 | style = MaterialTheme.typography.labelMedium 36 | ) 37 | Text( 38 | modifier = Modifier.padding(top = 8.dp), 39 | text = item.data, 40 | style = MaterialTheme.typography.bodyLarge, 41 | color = MaterialTheme.colorScheme.onSurfaceVariant 42 | ) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /UI/src/main/java/com/github/premnirmal/tickerwidget/ui/theme/AppTheme.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.tickerwidget.ui.theme 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.dynamicDarkColorScheme 7 | import androidx.compose.material3.dynamicLightColorScheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.platform.LocalContext 10 | 11 | @Composable fun AppTheme( 12 | theme: SelectedTheme, 13 | content: @Composable () -> Unit 14 | ) { 15 | val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 16 | val isDarkTheme = isSystemInDarkTheme() 17 | val colorScheme = when (theme) { 18 | SelectedTheme.SYSTEM -> { 19 | if (dynamicColor) { 20 | if (isDarkTheme) { 21 | dynamicDarkColorScheme(LocalContext.current) 22 | } else { 23 | dynamicLightColorScheme(LocalContext.current) 24 | } 25 | } else { 26 | if (isDarkTheme) ThemePref.Dark.colours.toColorScheme() else ThemePref.Light.colours.toColorScheme() 27 | } 28 | } 29 | 30 | SelectedTheme.LIGHT -> { 31 | if (dynamicColor) { 32 | dynamicLightColorScheme(LocalContext.current) 33 | } else { 34 | ThemePref.Light.colours.toColorScheme() 35 | } 36 | } 37 | SelectedTheme.DARK -> { 38 | if (dynamicColor) { 39 | dynamicDarkColorScheme(LocalContext.current) 40 | } else { 41 | ThemePref.Dark.colours.toColorScheme() 42 | } 43 | } 44 | } 45 | MaterialTheme( 46 | colorScheme = colorScheme, 47 | typography = AppTypography, 48 | shapes = AppShapes 49 | ) { 50 | content() 51 | } 52 | } 53 | 54 | enum class SelectedTheme { 55 | SYSTEM, 56 | LIGHT, 57 | DARK, 58 | } -------------------------------------------------------------------------------- /app/src/test/java/com/github/premnirmal/ticker/tools/Parser.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.tools 2 | 3 | import kotlinx.serialization.json.Json 4 | import kotlinx.serialization.json.JsonElement 5 | import kotlinx.serialization.json.decodeFromJsonElement 6 | import java.io.ByteArrayOutputStream 7 | import java.io.IOException 8 | import java.io.InputStream 9 | 10 | class Parser { 11 | val json = Json { 12 | ignoreUnknownKeys = true 13 | isLenient = true 14 | explicitNulls = false 15 | coerceInputValues = true 16 | prettyPrint = true 17 | } 18 | 19 | inline fun parseJsonFileType(fileName: String): T { 20 | val jsonFile = parseJsonFile(fileName) 21 | return json.decodeFromJsonElement(jsonFile) 22 | } 23 | 24 | fun parseJsonFile(resourceName: String): JsonElement { 25 | val jsonElement: JsonElement 26 | try { 27 | val `in` = javaClass.classLoader!!.getResourceAsStream(resourceName) ?: throw AssertionError( 28 | "Failed loading resource " + resourceName + " from " + Parser::class.java 29 | ) 30 | val jsonString = readInput(`in`) 31 | jsonElement = json.parseToJsonElement(jsonString) 32 | } catch (ioe: IOException) { 33 | throw RuntimeException("Parse failed", ioe) 34 | } 35 | 36 | return jsonElement 37 | } 38 | 39 | @Throws(IOException::class) 40 | private fun readInput(`in`: InputStream): String { 41 | val baos = ByteArrayOutputStream() 42 | val buffer = ByteArray(1024) 43 | var length = `in`.read(buffer) 44 | while (length != -1) { 45 | baos.write(buffer, 0, length) 46 | length = `in`.read(buffer) 47 | } 48 | `in`.close() 49 | return baos.toString() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/Position.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.Serializable 6 | 7 | @Parcelize 8 | @Serializable 9 | data class Position( 10 | var symbol: String = "", 11 | var holdings: MutableList = ArrayList() 12 | ) : Parcelable { 13 | 14 | fun add(holding: Holding) { 15 | holdings.add(holding) 16 | } 17 | 18 | fun remove(holding: Holding): Boolean { 19 | return holdings.remove(holding) 20 | } 21 | 22 | fun averagePrice(): Float { 23 | return holdings.averagePrice() 24 | } 25 | 26 | fun totalShares(): Float = holdings.totalShares() 27 | 28 | fun totalPaidPrice(): Float = holdings.totalPaidPrice() 29 | } 30 | 31 | fun List.totalShares(): Float = this.sumOf { it.shares.toDouble() }.toFloat() 32 | fun List.totalPaidPrice(): Float = this.sumOf { it.totalValue().toDouble() }.toFloat() 33 | fun List.averagePrice(): Float = if (this.totalShares() == 0f) 0f else this.totalPaidPrice() / this.totalShares() 34 | 35 | fun List.holdingsSum(): HoldingSum { 36 | val totalShares = this.totalShares() 37 | val totalPaidPrice = this.totalPaidPrice() 38 | val averagePrice = this.averagePrice() 39 | return HoldingSum(totalShares, totalPaidPrice, averagePrice) 40 | } 41 | 42 | @Parcelize 43 | data class HoldingSum( 44 | val totalShares: Float, 45 | val totalPaidPrice: Float, 46 | val averagePrice: Float, 47 | ) : Parcelable 48 | 49 | @Parcelize 50 | @Serializable 51 | data class Holding( 52 | val symbol: String, 53 | val shares: Float = 0.0f, 54 | val price: Float = 0.0f, 55 | var id: Long? = null 56 | ) : Parcelable { 57 | 58 | fun totalValue(): Float = shares * price 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_header.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 29 | 30 | 37 | 38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/layout/stockview2.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 32 | 33 | 34 | 45 | 46 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/CollectBottomSheetMessage.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.lifecycle.Lifecycle 11 | import androidx.lifecycle.LifecycleOwner 12 | import androidx.lifecycle.repeatOnLifecycle 13 | import com.github.premnirmal.ticker.ui.AppMessage.BottomSheetMessage 14 | import kotlinx.coroutines.delay 15 | import kotlinx.coroutines.isActive 16 | import java.util.LinkedList 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Composable 20 | fun LifecycleOwner.CollectBottomSheetMessage() { 21 | val appMessaging = LocalAppMessaging.current 22 | val bottomSheetMessageQueue = remember { 23 | LinkedList() 24 | } 25 | var bottomSheetMessage: BottomSheetMessage? by remember { 26 | mutableStateOf(null) 27 | } 28 | 29 | LaunchedEffect(Unit) { 30 | repeatOnLifecycle(Lifecycle.State.STARTED) { 31 | appMessaging.bottomSheets.collect { message -> 32 | bottomSheetMessageQueue.add(message) 33 | } 34 | } 35 | } 36 | 37 | LaunchedEffect(Unit) { 38 | repeatOnLifecycle(Lifecycle.State.STARTED) { 39 | while (isActive) { 40 | delay(600L) 41 | if (bottomSheetMessage == null) { 42 | bottomSheetMessage = bottomSheetMessageQueue.poll() 43 | } 44 | } 45 | } 46 | } 47 | 48 | bottomSheetMessage?.let { 49 | BottomSheetWithMessage(message = it) { 50 | bottomSheetMessage = null 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/widget/WidgetClickReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.widget 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import com.github.premnirmal.ticker.analytics.Analytics 8 | import com.github.premnirmal.ticker.analytics.ClickEvent 9 | import com.github.premnirmal.ticker.components.Injector 10 | import com.github.premnirmal.ticker.home.HomeActivity 11 | import javax.inject.Inject 12 | 13 | /** 14 | * Created by premnirmal on 2/27/16. 15 | */ 16 | class WidgetClickReceiver : BroadcastReceiver() { 17 | 18 | @Inject internal lateinit var widgetDataProvider: WidgetDataProvider 19 | 20 | @Inject internal lateinit var analytics: Analytics 21 | 22 | override fun onReceive( 23 | context: Context, 24 | intent: Intent 25 | ) { 26 | Injector.appComponent().inject(this) 27 | if (intent.getBooleanExtra(FLIP, false)) { 28 | val widgetId = intent.getIntExtra(WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) 29 | val widgetData = widgetDataProvider.dataForWidgetId(widgetId) 30 | widgetData.flipChange() 31 | widgetDataProvider.broadcastUpdateWidget(widgetId) 32 | analytics.trackClickEvent(ClickEvent("WidgetFlipClick")) 33 | } else { 34 | val startActivityIntent = Intent(context, HomeActivity::class.java) 35 | startActivityIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 36 | context.startActivity(startActivityIntent) 37 | analytics.trackClickEvent(ClickEvent("WidgetClick")) 38 | } 39 | } 40 | 41 | companion object { 42 | 43 | const val CLICK_BCAST_INTENTFILTER = "com.github.premnirmal.ticker.WIDGET_CLICK" 44 | const val FLIP = "FLIP" 45 | const val WIDGET_ID = AppWidgetManager.EXTRA_APPWIDGET_ID 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/WindowStateUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import android.graphics.Rect 4 | import androidx.compose.runtime.staticCompositionLocalOf 5 | import androidx.window.layout.FoldingFeature 6 | import kotlin.contracts.ExperimentalContracts 7 | import kotlin.contracts.contract 8 | 9 | sealed interface DevicePosture { 10 | object NormalPosture : DevicePosture 11 | 12 | data class BookPosture( 13 | val hingePosition: Rect 14 | ) : DevicePosture 15 | 16 | data class Separating( 17 | val hingePosition: Rect, 18 | var orientation: FoldingFeature.Orientation 19 | ) : DevicePosture 20 | } 21 | 22 | @OptIn(ExperimentalContracts::class) 23 | fun isBookPosture(foldFeature: FoldingFeature?): Boolean { 24 | contract { returns(true) implies (foldFeature != null) } 25 | return foldFeature?.state == FoldingFeature.State.HALF_OPENED && 26 | foldFeature.orientation == FoldingFeature.Orientation.VERTICAL 27 | } 28 | 29 | @OptIn(ExperimentalContracts::class) 30 | fun isSeparating(foldFeature: FoldingFeature?): Boolean { 31 | contract { returns(true) implies (foldFeature != null) } 32 | return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating 33 | } 34 | 35 | /** 36 | * Different type of navigation supported by app depending on device size and state. 37 | */ 38 | enum class NavigationType { 39 | BOTTOM_NAVIGATION, NAVIGATION_RAIL 40 | } 41 | 42 | /** 43 | * Different position of navigation content inside Navigation Rail, Navigation Drawer depending on device size and state. 44 | */ 45 | enum class NavigationContentPosition { 46 | TOP, CENTER 47 | } 48 | 49 | /** 50 | * App Content shown depending on device size and state. 51 | */ 52 | enum class ContentType { 53 | SINGLE_PANE, DUAL_PANE 54 | } 55 | 56 | val LocalContentType = staticCompositionLocalOf { 57 | error("No contentType sender provided") 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/prod/java/com/github/premnirmal/ticker/analytics/AnalyticsImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.analytics 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.os.Bundle 6 | import com.github.premnirmal.ticker.home.HomeActivity 7 | import com.google.firebase.analytics.FirebaseAnalytics 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | 10 | /** 11 | * Created by premnirmal on 2/26/16. 12 | */ 13 | class AnalyticsImpl( 14 | @ApplicationContext private val context: Context, 15 | private val generalProperties: dagger.Lazy 16 | ) : Analytics { 17 | 18 | private val firebaseAnalytics: FirebaseAnalytics by lazy { 19 | FirebaseAnalytics.getInstance(context) 20 | } 21 | 22 | override fun trackScreenView(screenName: String, activity: Activity) { 23 | if (activity is HomeActivity) { 24 | firebaseAnalytics.logEvent(FirebaseAnalytics.Event.APP_OPEN, null) 25 | } 26 | firebaseAnalytics.setCurrentScreen(activity, screenName, null) 27 | val bundle = Bundle().apply { 28 | putString(FirebaseAnalytics.Param.ITEM_NAME, screenName) 29 | putInt("WidgetCount", generalProperties.get().widgetCount) 30 | putInt("TickerCount", generalProperties.get().tickerCount) 31 | } 32 | firebaseAnalytics.logEvent("ScreenView", bundle) 33 | } 34 | 35 | override fun trackClickEvent(event: ClickEvent) { 36 | firebaseAnalytics.logEvent(event.name, fromEvent(event)) 37 | } 38 | 39 | override fun trackGeneralEvent(event: GeneralEvent) { 40 | firebaseAnalytics.logEvent(event.name, fromEvent(event)) 41 | } 42 | 43 | private fun fromEvent(event: AnalyticsEvent): Bundle { 44 | return Bundle().apply { 45 | putString(FirebaseAnalytics.Param.ITEM_NAME, event.name) 46 | event.properties.forEach { entry -> 47 | putString(entry.key, entry.value) 48 | } 49 | putInt("WidgetCount", generalProperties.get().widgetCount) 50 | putInt("TickerCount", generalProperties.get().tickerCount) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 29 | 30 | 31 | 33 | 34 | 36 | 37 | 39 | 40 | 42 | 43 | 45 | 46 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/stockview.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 32 | 33 | 40 | 41 | 51 | 52 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pequeno 6 | Médio 7 | Grande 8 | 9 | 10 | 0 11 | 1 12 | 2 13 | 14 | 15 | 16 | Claro 17 | Escuro 18 | Como o sistema 19 | 20 | 21 | 22 | Animada 23 | Separadores 24 | Fixa 25 | Meu portfólio 26 | 27 | 28 | 29 | 0 30 | 1 31 | 2 32 | 33 | 34 | 35 | Como o sistema 36 | Transparente 37 | Translucente 38 | 39 | 40 | 41 | Como o sistema 42 | Branco 43 | Preto 44 | 45 | 46 | 47 | 5 minutos 48 | 15 minutos 49 | 30 minutos 50 | 45 minutos 51 | 1 hora 52 | 53 | 54 | 0 55 | 1 56 | 2 57 | 3 58 | 4 59 | 60 | 61 | 62 | Segunda-Feira 63 | Terça-Feira 64 | Quarta-Feira 65 | Quinta-Feira 66 | Sexta-Feira 67 | Sábado 68 | Domingo 69 | 70 | 71 | 1 72 | 2 73 | 3 74 | 4 75 | 5 76 | 6 77 | 7 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/premnirmal/ticker/BaseUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker 2 | 3 | import com.github.premnirmal.ticker.mock.Mocker 4 | import com.github.premnirmal.ticker.model.FetchResult 5 | import com.github.premnirmal.ticker.model.StocksProvider 6 | import com.github.premnirmal.ticker.model.StocksProvider.FetchState 7 | import com.github.premnirmal.ticker.tools.Parser 8 | import com.nhaarman.mockitokotlin2.any 9 | import com.nhaarman.mockitokotlin2.doNothing 10 | import com.nhaarman.mockitokotlin2.whenever 11 | import dagger.hilt.android.testing.HiltAndroidRule 12 | import dagger.hilt.android.testing.HiltTestApplication 13 | import junit.framework.TestCase 14 | import kotlinx.coroutines.flow.MutableStateFlow 15 | import kotlinx.coroutines.test.runTest 16 | import kotlinx.serialization.json.JsonElement 17 | import org.junit.Before 18 | import org.junit.Rule 19 | import org.junit.runner.RunWith 20 | import org.robolectric.RobolectricTestRunner 21 | import org.robolectric.annotation.Config 22 | 23 | /** 24 | * Created by premnirmal on 3/22/17. 25 | */ 26 | @RunWith(RobolectricTestRunner::class) 27 | @Config(application = HiltTestApplication::class) 28 | abstract class BaseUnitTest : TestCase() { 29 | 30 | @get:Rule 31 | var hiltRule = HiltAndroidRule(this) 32 | 33 | private val parser = Parser() 34 | 35 | @Before public override fun setUp() = runTest { 36 | super.setUp() 37 | val iStocksProvider = Mocker.provide(StocksProvider::class) 38 | doNothing().whenever(iStocksProvider).schedule() 39 | whenever(iStocksProvider.fetch()).thenReturn(FetchResult.success(ArrayList())) 40 | whenever(iStocksProvider.tickers).thenReturn(MutableStateFlow((emptyList()))) 41 | whenever(iStocksProvider.addStock(any())).thenReturn(emptyList()) 42 | whenever(iStocksProvider.fetchState).thenReturn(MutableStateFlow(FetchState.NotFetched)) 43 | whenever(iStocksProvider.nextFetchMs).thenReturn(MutableStateFlow(0L)) 44 | } 45 | 46 | fun parseJsonFile(fileName: String): JsonElement { 47 | return parser.parseJsonFile(fileName) 48 | } 49 | 50 | internal inline fun parseJsonFile(fileName: String): T { 51 | return parser.parseJsonFileType(fileName) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /UI/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | alias(libs.plugins.compose.compiler) 5 | alias(libs.plugins.detekt.plugin) 6 | } 7 | 8 | detekt { 9 | toolVersion = libs.versions.detekt.get() 10 | config.setFrom(file("../config/detekt/detekt.yml")) 11 | buildUponDefaultConfig = true 12 | autoCorrect = true 13 | } 14 | 15 | buildscript { 16 | repositories { 17 | mavenCentral() 18 | google() 19 | maven("https://oss.sonatype.org/content/repositories/snapshots/") 20 | maven("https://jitpack.io") 21 | } 22 | dependencies { 23 | classpath(kotlin("gradle-plugin", version = "2.0.0")) 24 | } 25 | } 26 | 27 | repositories { 28 | mavenCentral() 29 | maven("https://oss.sonatype.org/content/repositories/snapshots/") 30 | maven("https://jitpack.io") 31 | maven("https://maven.google.com") 32 | } 33 | 34 | android { 35 | namespace = "com.github.premnirmal.tickerwidget.ui" 36 | compileSdk = 36 37 | 38 | defaultConfig { 39 | minSdk = 26 40 | targetSdk = 36 41 | 42 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 43 | consumerProguardFiles("consumer-rules.pro") 44 | } 45 | 46 | buildTypes { 47 | release { 48 | isMinifyEnabled = false 49 | setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) 50 | } 51 | } 52 | buildFeatures { 53 | compose = true 54 | } 55 | composeOptions { 56 | kotlinCompilerExtensionVersion = "1.5.8" 57 | } 58 | 59 | compileOptions { 60 | sourceCompatibility = JavaVersion.VERSION_17 61 | targetCompatibility = JavaVersion.VERSION_17 62 | } 63 | kotlinOptions { 64 | jvmTarget = JavaVersion.VERSION_17.toString() 65 | } 66 | } 67 | 68 | dependencies { 69 | implementation(kotlin("stdlib")) 70 | 71 | implementation(AndroidX.core.ktx) 72 | implementation(AndroidX.appCompat) 73 | implementation(libs.androidx.compose.runtime) 74 | implementation(libs.androidx.compose.foundation) 75 | implementation(libs.androidx.compose.ui.ui) 76 | implementation(libs.androidx.compose.material3) 77 | 78 | implementation(AndroidX.compose.runtime) 79 | implementation(AndroidX.compose.runtime.liveData) 80 | implementation(AndroidX.navigation.compose) 81 | 82 | testImplementation(Testing.junit4) 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/settings/ExportTasks.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.settings 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import com.github.premnirmal.ticker.components.Injector 6 | import com.github.premnirmal.ticker.network.data.Quote 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import timber.log.Timber 10 | import java.io.FileOutputStream 11 | import java.io.IOException 12 | 13 | internal object TickersExporter { 14 | 15 | suspend fun exportTickers(context: Context, uri: Uri, vararg tickers: List): String? = withContext( 16 | Dispatchers.IO 17 | ) { 18 | val tickerList = ArrayList(tickers[0]) 19 | val contentResolver = context.applicationContext.contentResolver 20 | try { 21 | contentResolver.openFileDescriptor(uri, "w")?.use { 22 | FileOutputStream(it.fileDescriptor).use { fileOutputStream -> 23 | for (ticker in tickerList) { 24 | fileOutputStream.write(("$ticker, ").toByteArray()) 25 | } 26 | } 27 | } 28 | } catch (e: IOException) { 29 | Timber.e(e) 30 | return@withContext null 31 | } 32 | 33 | return@withContext uri.path 34 | } 35 | } 36 | 37 | internal object PortfolioExporter { 38 | 39 | private val json = Injector.appComponent().json() 40 | 41 | suspend fun exportQuotes(context: Context, uri: Uri, vararg quoteLists: List): String? = 42 | withContext(Dispatchers.IO) { 43 | val quoteList: List = quoteLists[0] 44 | val jsonString = json.encodeToString(quoteList) 45 | val contentResolver = context.applicationContext.contentResolver 46 | try { 47 | contentResolver.openFileDescriptor(uri, "rwt") 48 | ?.use { 49 | FileOutputStream(it.fileDescriptor).use { fileOutputStream -> 50 | fileOutputStream.write(jsonString.toByteArray()) 51 | } 52 | } 53 | } catch (e: IOException) { 54 | Timber.e(e) 55 | return@withContext null 56 | } 57 | return@withContext uri.path 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/repo/data/QuoteRow.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.repo.data 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity 8 | data class QuoteRow( 9 | @PrimaryKey @ColumnInfo(name = "symbol") val symbol: String, 10 | @ColumnInfo(name = "name") val name: String, 11 | @ColumnInfo(name = "last_trade_price") val lastTradePrice: Float, 12 | @ColumnInfo(name = "change_percent") val changeInPercent: Float, 13 | @ColumnInfo(name = "change") val change: Float, 14 | @ColumnInfo(name = "exchange") val stockExchange: String, 15 | @ColumnInfo(name = "currency") val currency: String, 16 | @ColumnInfo(name = "is_post_market") val isPostMarket: Boolean, 17 | @ColumnInfo(name = "annual_dividend_rate") val annualDividendRate: Float, 18 | @ColumnInfo(name = "annual_dividend_yield") val annualDividendYield: Float, 19 | @ColumnInfo(name = "dayHigh") val dayHigh: Float?, 20 | @ColumnInfo(name = "dayLow") val dayLow: Float?, 21 | @ColumnInfo(name = "previousClose") val previousClose: Float, 22 | @ColumnInfo(name = "open") val open: Float?, 23 | @ColumnInfo(name = "regularMarketVolume") val regularMarketVolume: Float?, 24 | @ColumnInfo(name = "peRatio") val peRatio: Float?, 25 | @ColumnInfo(name = "fiftyTwoWeekLowChange") val fiftyTwoWeekLowChange: Float?, 26 | @ColumnInfo(name = "fiftyTwoWeekLowChangePercent") val fiftyTwoWeekLowChangePercent: Float?, 27 | @ColumnInfo(name = "fiftyTwoWeekHighChange") val fiftyTwoWeekHighChange: Float?, 28 | @ColumnInfo(name = "fiftyTwoWeekHighChangePercent") val fiftyTwoWeekHighChangePercent: Float?, 29 | @ColumnInfo(name = "fiftyTwoWeekLow") val fiftyTwoWeekLow: Float?, 30 | @ColumnInfo(name = "fiftyTwoWeekHigh") val fiftyTwoWeekHigh: Float?, 31 | @ColumnInfo(name = "dividendDate") val dividendDate: Float?, 32 | @ColumnInfo(name = "earningsDate") val earningsDate: Float?, 33 | @ColumnInfo(name = "marketCap") val marketCap: Float?, 34 | @ColumnInfo(name = "isTradeable") val isTradeable: Boolean?, 35 | @ColumnInfo(name = "isTriggerable") val isTriggerable: Boolean?, 36 | @ColumnInfo(name = "marketState") val marketState: String?, 37 | @ColumnInfo(name = "fiftyDayAverage") val fiftyDayAverage: Float?, 38 | @ColumnInfo(name = "twoHundredDayAverage") val twoHundredDayAverage: Float?, 39 | ) 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/UIStates.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.CircularProgressIndicator 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.text.style.TextAlign 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import androidx.compose.ui.unit.dp 16 | import com.github.premnirmal.tickerwidget.R.string 17 | 18 | @Composable 19 | fun EmptyState( 20 | modifier: Modifier = Modifier, 21 | contentAlignment: Alignment = Alignment.Center, 22 | text: String 23 | ) { 24 | Box( 25 | modifier = modifier.fillMaxSize(), 26 | contentAlignment = contentAlignment 27 | ) { 28 | Text( 29 | modifier = Modifier.padding(8.dp), 30 | text = text, 31 | style = MaterialTheme.typography.titleLarge, 32 | textAlign = TextAlign.Center, 33 | color = MaterialTheme.colorScheme.primary 34 | ) 35 | } 36 | } 37 | 38 | @Composable 39 | fun ErrorState( 40 | modifier: Modifier = Modifier, 41 | text: String 42 | ) { 43 | Box( 44 | modifier = modifier.fillMaxSize(), 45 | contentAlignment = Alignment.Center 46 | ) { 47 | Text( 48 | modifier = Modifier.padding(8.dp), 49 | text = text, 50 | style = MaterialTheme.typography.titleMedium, 51 | textAlign = TextAlign.Center, 52 | color = MaterialTheme.colorScheme.error 53 | ) 54 | } 55 | } 56 | 57 | @Composable 58 | fun ProgressState(modifier: Modifier = Modifier) { 59 | Box( 60 | modifier = modifier.fillMaxSize(), 61 | contentAlignment = Alignment.Center 62 | ) { 63 | CircularProgressIndicator() 64 | } 65 | } 66 | 67 | @Preview 68 | @Composable 69 | fun ErrorStatePreview() { 70 | ErrorState( 71 | modifier = Modifier 72 | .fillMaxSize() 73 | .padding(horizontal = 8.dp), 74 | text = stringResource(id = string.no_data) 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/AxisFormatters.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import android.graphics.Canvas 4 | import com.github.mikephil.charting.components.AxisBase 5 | import com.github.mikephil.charting.components.XAxis 6 | import com.github.mikephil.charting.formatter.IAxisValueFormatter 7 | import com.github.mikephil.charting.renderer.XAxisRenderer 8 | import com.github.mikephil.charting.utils.MPPointF 9 | import com.github.mikephil.charting.utils.Transformer 10 | import com.github.mikephil.charting.utils.Utils 11 | import com.github.mikephil.charting.utils.ViewPortHandler 12 | import com.github.premnirmal.ticker.AppPreferences 13 | import java.time.Instant 14 | import java.time.LocalDateTime 15 | import java.time.ZoneId 16 | 17 | class DateAxisFormatter : IAxisValueFormatter { 18 | 19 | override fun getFormattedValue( 20 | value: Float, 21 | axis: AxisBase 22 | ): String { 23 | val date = LocalDateTime.ofInstant(Instant.ofEpochSecond(value.toLong()), ZoneId.systemDefault()).toLocalDate() 24 | return date.format(AppPreferences.AXIS_DATE_FORMATTER) 25 | } 26 | } 27 | 28 | class HourAxisFormatter : IAxisValueFormatter { 29 | 30 | override fun getFormattedValue( 31 | value: Float, 32 | axis: AxisBase? 33 | ): String { 34 | val hour = LocalDateTime.ofInstant(Instant.ofEpochSecond(value.toLong()), ZoneId.systemDefault()).toLocalTime() 35 | return hour.format(AppPreferences.TIME_FORMATTER) 36 | } 37 | } 38 | 39 | class ValueAxisFormatter : IAxisValueFormatter { 40 | 41 | override fun getFormattedValue( 42 | value: Float, 43 | axis: AxisBase 44 | ): String = 45 | AppPreferences.DECIMAL_FORMAT.format(value) 46 | } 47 | 48 | class MultilineXAxisRenderer( 49 | viewPortHandler: ViewPortHandler?, 50 | xAxis: XAxis?, 51 | trans: Transformer? 52 | ) : XAxisRenderer(viewPortHandler, xAxis, trans) { 53 | 54 | override fun drawLabel( 55 | c: Canvas, 56 | formattedLabel: String, 57 | x: Float, 58 | y: Float, 59 | anchor: MPPointF, 60 | angleDegrees: Float 61 | ) { 62 | val lines = formattedLabel.split("-") 63 | for (i in 0 until lines.size) { 64 | val vOffset = i * mAxisLabelPaint.textSize 65 | Utils.drawXAxisValue(c, lines[i], x, y + vOffset, mAxisLabelPaint, anchor, angleDegrees) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/news/NewsFeedViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.news 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.github.premnirmal.ticker.model.FetchResult 6 | import com.github.premnirmal.ticker.network.NewsProvider 7 | import com.github.premnirmal.ticker.news.NewsFeedItem.ArticleNewsFeed 8 | import com.github.premnirmal.ticker.news.NewsFeedItem.TrendingStockNewsFeed 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.StateFlow 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.withContext 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class NewsFeedViewModel @Inject constructor( 19 | private val newsProvider: NewsProvider 20 | ) : ViewModel() { 21 | 22 | val newsFeedFlow: StateFlow> 23 | get() = _newsFeedFlow 24 | private val _newsFeedFlow = MutableStateFlow>(emptyList()) 25 | 26 | val isRefreshing: StateFlow 27 | get() = _isRefreshing 28 | private val _isRefreshing = MutableStateFlow(false) 29 | 30 | val newsFeed: StateFlow>?> 31 | get() = _newsFeed 32 | private val _newsFeed = MutableStateFlow>?>(null) 33 | 34 | fun fetchNews(forceRefresh: Boolean = false) { 35 | _isRefreshing.value = true 36 | viewModelScope.launch { 37 | val news = newsProvider.fetchMarketNews(useCache = !forceRefresh) 38 | val trending = newsProvider.fetchTrendingStocks(useCache = !forceRefresh) 39 | if (news.wasSuccessful) { 40 | val data = ArrayList() 41 | withContext(Dispatchers.Default) { 42 | data.addAll(news.data.map { ArticleNewsFeed(it) }) 43 | if (trending.wasSuccessful) { 44 | val taken = trending.data.take(6) 45 | data.add(0, TrendingStockNewsFeed(taken)) 46 | } 47 | } 48 | _newsFeed.value = FetchResult.success(data) 49 | _newsFeedFlow.emit(data) 50 | } else { 51 | _newsFeed.value = FetchResult.failure(news.error) 52 | } 53 | _isRefreshing.value = false 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/res/values-it/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Piccolo 6 | Medio 7 | Grande 8 | 9 | 10 | 0 11 | 1 12 | 2 13 | 14 | 15 | 16 | Chiaro 17 | Scuro 18 | Come da sistema 19 | 20 | 21 | 22 | Animato 23 | Etichette 24 | Fisso 25 | Il mio portfolio 26 | 27 | 28 | 29 | 0 30 | 1 31 | 2 32 | 33 | 34 | 35 | Default 36 | Un\'azione per riga 37 | 38 | 39 | 0 40 | 1 41 | 42 | 43 | 44 | Come da sistema 45 | Trasparente 46 | Translucent 47 | 48 | 49 | 50 | Come da sistema 51 | Bianco 52 | Nero 53 | 54 | 55 | 56 | 5 minuti 57 | 15 minuti 58 | 30 minuti 59 | 45 minuti 60 | 1 ora 61 | 62 | 63 | 0 64 | 1 65 | 2 66 | 3 67 | 4 68 | 69 | 70 | 71 | Lunedì 72 | Martedì 73 | Mercoledì 74 | Giovedì 75 | Venerdì 76 | Sabato 77 | Domenica 78 | 79 | 80 | 1 81 | 2 82 | 3 83 | 4 84 | 5 85 | 6 86 | 7 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 12dp 3 | 12dp 4 | 24dp 5 | 12dp 6 | 7 | 8000 8 | 2dp 9 | 5dp 10 | 11 | 16dp 12 | 14sp 13 | 11sp 14 | 8dp 15 | 16 | 12 17 | 14 18 | 16 19 | 20 | 3dp 21 | 22 | 14sp 23 | 11sp 24 | 11sp 25 | 10sp 26 | 27 | 10sp 28 | 8sp 29 | 15sp 30 | 16sp 31 | 18sp 32 | 20sp 33 | 14sp 34 | 35 | 2dp 36 | 20dp 37 | 38 | 16dp 39 | 16dp 40 | 275dp 41 | 200dp 42 | 8dp 43 | 8dp 44 | 45 | 16dp 46 | 12dp 47 | 12dp 48 | 16dp 49 | 65dp 50 | 130dp 51 | 52 | 6dp 53 | 20dp 54 | 4dp 55 | 2dp 56 | 57 | 600dp 58 | 125dp 59 | 60 | -------------------------------------------------------------------------------- /UI/src/main/java/com/github/premnirmal/tickerwidget/ui/theme/AppColours.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.tickerwidget.ui.theme 2 | 3 | import androidx.compose.material3.ColorScheme 4 | import androidx.compose.ui.graphics.Color 5 | 6 | internal data class AppColours( 7 | val primary: Color, 8 | val onPrimary: Color, 9 | val primaryContainer: Color, 10 | val onPrimaryContainer: Color, 11 | val inversePrimary: Color, 12 | val secondary: Color, 13 | val onSecondary: Color, 14 | val secondaryContainer: Color, 15 | val onSecondaryContainer: Color, 16 | val tertiary: Color, 17 | val onTertiary: Color, 18 | val tertiaryContainer: Color, 19 | val onTertiaryContainer: Color, 20 | val background: Color, 21 | val onBackground: Color, 22 | val surface: Color, 23 | val onSurface: Color, 24 | val surfaceVariant: Color, 25 | val onSurfaceVariant: Color, 26 | val surfaceTint: Color, 27 | val inverseSurface: Color, 28 | val inverseOnSurface: Color, 29 | val error: Color, 30 | val onError: Color, 31 | val errorContainer: Color, 32 | val onErrorContainer: Color, 33 | val outline: Color, 34 | val outlineVariant: Color, 35 | val scrim: Color, 36 | ) 37 | 38 | internal fun AppColours.toColorScheme(): ColorScheme { 39 | return ColorScheme( 40 | primary = this.primary, 41 | onPrimary = this.onPrimary, 42 | primaryContainer = this.primaryContainer, 43 | onPrimaryContainer = this.onPrimaryContainer, 44 | inversePrimary = this.inversePrimary, 45 | secondary = this.secondary, 46 | onSecondary = this.onSecondary, 47 | secondaryContainer = this.secondaryContainer, 48 | onSecondaryContainer = this.onSecondaryContainer, 49 | tertiary = this.tertiary, 50 | onTertiary = this.onTertiary, 51 | tertiaryContainer = this.tertiaryContainer, 52 | onTertiaryContainer = this.onTertiaryContainer, 53 | background = this.background, 54 | onBackground = this.onBackground, 55 | surface = this.surface, 56 | onSurface = this.onSurface, 57 | surfaceVariant = this.surfaceVariant, 58 | onSurfaceVariant = this.onSurfaceVariant, 59 | surfaceTint = this.surfaceTint, 60 | inverseSurface = this.inverseSurface, 61 | inverseOnSurface = this.inverseOnSurface, 62 | error = this.error, 63 | onError = this.onError, 64 | errorContainer = this.errorContainer, 65 | onErrorContainer = this.onErrorContainer, 66 | outline = this.outline, 67 | outlineVariant = this.outlineVariant, 68 | scrim = this.scrim, 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/home/TotalHoldingsPopup.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.home 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.wrapContentSize 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Surface 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.window.Popup 18 | import androidx.compose.ui.window.PopupProperties 19 | import com.github.premnirmal.tickerwidget.R 20 | import com.github.premnirmal.tickerwidget.ui.theme.ColourPalette 21 | 22 | @Composable 23 | fun TotalHoldingsPopup( 24 | totalHoldings: HomeViewModel.TotalGainLoss, 25 | onDismiss: () -> Unit, 26 | ) { 27 | Popup( 28 | alignment = Alignment.TopEnd, 29 | properties = PopupProperties( 30 | excludeFromSystemGesture = true, 31 | ), 32 | onDismissRequest = onDismiss, 33 | ) { 34 | Surface( 35 | modifier = Modifier 36 | .wrapContentSize() 37 | .padding(8.dp) 38 | .background(MaterialTheme.colorScheme.surfaceVariant), 39 | shadowElevation = 4.dp, 40 | ) { 41 | Column( 42 | modifier = Modifier 43 | .padding(8.dp) 44 | ) { 45 | Text( 46 | modifier = Modifier.padding(bottom = 8.dp), 47 | text = stringResource(R.string.total_holdings, totalHoldings.holdings), 48 | color = MaterialTheme.colorScheme.onSurfaceVariant, 49 | ) 50 | Row( 51 | horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), 52 | ) { 53 | Text( 54 | text = totalHoldings.gain, 55 | color = ColourPalette.PositiveGreen, 56 | ) 57 | Text( 58 | text = totalHoldings.loss, 59 | color = ColourPalette.NegativeRed, 60 | ) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/AppMessaging.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import android.content.Context 4 | import androidx.compose.material3.SnackbarHostState 5 | import androidx.compose.runtime.staticCompositionLocalOf 6 | import com.github.premnirmal.ticker.ui.AppMessage.BottomSheetMessage 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.MutableSharedFlow 11 | import kotlinx.coroutines.flow.filterIsInstance 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | import javax.inject.Singleton 15 | 16 | @Singleton 17 | class AppMessaging @Inject constructor( 18 | @ApplicationContext private val context: Context, 19 | private val coroutineScope: CoroutineScope, 20 | ) { 21 | 22 | val snackbarHostState = SnackbarHostState() 23 | val bottomSheets: Flow 24 | get() = _messageQueue.filterIsInstance(BottomSheetMessage::class) 25 | private val _messageQueue = MutableSharedFlow(replay = 0, extraBufferCapacity = 100) 26 | 27 | fun sendSnackbar( 28 | message: Int, 29 | ) { 30 | coroutineScope.launch { 31 | snackbarHostState.showSnackbar( 32 | context.getString(message) 33 | ) 34 | } 35 | } 36 | 37 | fun sendSnackbar( 38 | message: String, 39 | ) { 40 | coroutineScope.launch { 41 | snackbarHostState.showSnackbar( 42 | message 43 | ) 44 | } 45 | } 46 | 47 | fun sendBottomSheet( 48 | title: Int, 49 | message: String, 50 | ) { 51 | coroutineScope.launch { 52 | _messageQueue.emit( 53 | BottomSheetMessage( 54 | title = context.getString(title), 55 | message = message, 56 | ) 57 | ) 58 | } 59 | } 60 | 61 | fun sendBottomSheet( 62 | title: String, 63 | message: String, 64 | ) { 65 | coroutineScope.launch { 66 | _messageQueue.emit(BottomSheetMessage(title = title, message = message)) 67 | } 68 | } 69 | } 70 | 71 | sealed class AppMessage( 72 | val title: String, 73 | val message: String, 74 | ) { 75 | class BottomSheetMessage( 76 | title: String, 77 | message: String, 78 | ) : AppMessage(title, message) 79 | } 80 | 81 | val LocalAppMessaging = staticCompositionLocalOf { 82 | error("No AppMessaging sender provided") 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/navigation/NavigationHelpers.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.navigation 2 | 3 | import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass 4 | import androidx.compose.runtime.Composable 5 | import androidx.window.layout.DisplayFeature 6 | import androidx.window.layout.FoldingFeature 7 | import com.github.premnirmal.ticker.ui.ContentType 8 | import com.github.premnirmal.ticker.ui.DevicePosture 9 | import com.github.premnirmal.ticker.ui.NavigationType 10 | import com.github.premnirmal.ticker.ui.isBookPosture 11 | import com.github.premnirmal.ticker.ui.isSeparating 12 | 13 | enum class LayoutType { 14 | HEADER, 15 | CONTENT 16 | } 17 | 18 | @Composable 19 | fun calculateContentAndNavigationType( 20 | widthSizeClass: WindowWidthSizeClass, 21 | displayFeatures: List 22 | ): Pair { 23 | /** 24 | * We are using display's folding features to map the device postures a fold is in. 25 | * In the state of folding device If it's half fold in BookPosture we want to avoid content 26 | * at the crease/hinge 27 | */ 28 | val foldingFeature = displayFeatures.filterIsInstance() 29 | .firstOrNull() 30 | val foldingDevicePosture = when { 31 | isBookPosture(foldingFeature) -> 32 | DevicePosture.BookPosture(foldingFeature.bounds) 33 | 34 | isSeparating(foldingFeature) -> 35 | DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation) 36 | 37 | else -> DevicePosture.NormalPosture 38 | } 39 | val contentType: ContentType 40 | val navigationType: NavigationType 41 | when (widthSizeClass) { 42 | WindowWidthSizeClass.Compact -> { 43 | navigationType = NavigationType.BOTTOM_NAVIGATION 44 | contentType = ContentType.SINGLE_PANE 45 | } 46 | WindowWidthSizeClass.Medium -> { 47 | navigationType = NavigationType.BOTTOM_NAVIGATION 48 | contentType = if (foldingDevicePosture != DevicePosture.NormalPosture) { 49 | ContentType.DUAL_PANE 50 | } else { 51 | ContentType.SINGLE_PANE 52 | } 53 | } 54 | WindowWidthSizeClass.Expanded -> { 55 | navigationType = NavigationType.NAVIGATION_RAIL 56 | contentType = ContentType.DUAL_PANE 57 | } 58 | else -> { 59 | navigationType = NavigationType.BOTTOM_NAVIGATION 60 | contentType = ContentType.SINGLE_PANE 61 | } 62 | } 63 | return Pair(navigationType, contentType) 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/portfolio/DecimalFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.portfolio 2 | 3 | import android.icu.text.DecimalFormatSymbols 4 | import androidx.compose.ui.text.AnnotatedString 5 | import androidx.compose.ui.text.input.OffsetMapping 6 | import androidx.compose.ui.text.input.TransformedText 7 | import androidx.compose.ui.text.input.VisualTransformation 8 | 9 | class DecimalFormatter( 10 | symbols: DecimalFormatSymbols = DecimalFormatSymbols.getInstance() 11 | ) { 12 | private val decimalSeparator = symbols.decimalSeparator 13 | 14 | fun cleanup(input: String): String { 15 | if (input.matches("\\D".toRegex())) return "" 16 | if (input.matches("0+".toRegex())) return "0" 17 | val sb = StringBuilder() 18 | var hasDecimalSep = false 19 | for (char in input) { 20 | if (char.isDigit()) { 21 | sb.append(char) 22 | continue 23 | } 24 | if (char == decimalSeparator && !hasDecimalSep && sb.isNotEmpty()) { 25 | sb.append(char) 26 | hasDecimalSep = true 27 | } 28 | } 29 | 30 | return sb.toString() 31 | } 32 | 33 | fun formatForVisual(input: String): String { 34 | val split = input.split(decimalSeparator) 35 | val intPart = split[0] 36 | val fractionPart = split.getOrNull(1) 37 | return if (fractionPart == null) intPart else intPart + decimalSeparator + fractionPart 38 | } 39 | } 40 | 41 | class DecimalInputVisualTransformation( 42 | private val decimalFormatter: DecimalFormatter 43 | ) : VisualTransformation { 44 | 45 | override fun filter(text: AnnotatedString): TransformedText { 46 | val inputText = text.text 47 | val formattedNumber = decimalFormatter.formatForVisual(inputText) 48 | val newText = AnnotatedString( 49 | text = formattedNumber, 50 | spanStyles = text.spanStyles, 51 | paragraphStyles = text.paragraphStyles 52 | ) 53 | val offsetMapping = FixedCursorOffsetMapping( 54 | contentLength = inputText.length, 55 | formattedContentLength = formattedNumber.length 56 | ) 57 | return TransformedText(newText, offsetMapping) 58 | } 59 | } 60 | 61 | private class FixedCursorOffsetMapping( 62 | private val contentLength: Int, 63 | private val formattedContentLength: Int, 64 | ) : OffsetMapping { 65 | override fun originalToTransformed(offset: Int): Int = formattedContentLength 66 | override fun transformedToOriginal(offset: Int): Int = contentLength 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/res/values/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Small 6 | Medium 7 | Large 8 | 9 | 10 | 0 11 | 1 12 | 2 13 | 14 | 15 | 16 | Light 17 | Dark 18 | Follow system 19 | 20 | 21 | 22 | 0 23 | 1 24 | 2 25 | 3 26 | 27 | 28 | 29 | Animated 30 | Tabs 31 | Fixed 32 | My portfolio 33 | 34 | 35 | 36 | 0 37 | 1 38 | 2 39 | 40 | 41 | 42 | Default 43 | Single stock in a row 44 | 45 | 46 | 0 47 | 1 48 | 49 | 50 | 51 | System 52 | Transparent 53 | Translucent 54 | 55 | 56 | 57 | System 58 | Light 59 | Dark 60 | 61 | 62 | 63 | 5 minutes 64 | 15 minutes 65 | 30 minutes 66 | 45 minutes 67 | 1 hour 68 | 69 | 70 | 0 71 | 1 72 | 2 73 | 3 74 | 4 75 | 76 | 77 | 78 | Monday 79 | Tuesday 80 | Wednesday 81 | Thursday 82 | Friday 83 | Saturday 84 | Sunday 85 | 86 | 87 | 1 88 | 2 89 | 3 90 | 4 91 | 5 92 | 6 93 | 7 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/portfolio/AddPositionViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.portfolio 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.github.premnirmal.ticker.model.StocksProvider 6 | import com.github.premnirmal.ticker.network.data.Holding 7 | import com.github.premnirmal.ticker.network.data.Position 8 | import com.github.premnirmal.ticker.network.data.Quote 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.MutableSharedFlow 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import kotlinx.coroutines.launch 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class AddPositionViewModel @Inject constructor(private val stocksProvider: StocksProvider) : ViewModel() { 19 | 20 | val position: StateFlow 21 | get() = _position 22 | private val _position = MutableStateFlow(Position("")) 23 | val quote: StateFlow 24 | get() = _quote 25 | private val _quote = MutableStateFlow(null) 26 | 27 | val removedHolding: Flow 28 | get() = _removedHolding 29 | private val _removedHolding = MutableSharedFlow() 30 | 31 | val addedHolding: Flow 32 | get() = _addedHolding 33 | private val _addedHolding = MutableSharedFlow() 34 | 35 | fun loadQuote(symbol: String) { 36 | viewModelScope.launch { 37 | loadQuoteInternal(symbol) 38 | } 39 | } 40 | 41 | fun removeHolding(symbol: String, holding: Holding) { 42 | viewModelScope.launch { 43 | val removed = stocksProvider.removePosition(symbol, holding) 44 | loadQuoteInternal(symbol) 45 | if (removed) { 46 | _removedHolding.emit(holding) 47 | } 48 | } 49 | } 50 | 51 | fun addHolding(symbol: String, shares: Float, price: Float) { 52 | viewModelScope.launch { 53 | val holding = stocksProvider.addHolding(symbol, shares, price) 54 | loadQuoteInternal(symbol) 55 | _addedHolding.emit(holding) 56 | } 57 | } 58 | 59 | private fun loadQuoteInternal(symbol: String) { 60 | _quote.value = getQuote(symbol) 61 | _position.value = getPosition(symbol) 62 | } 63 | 64 | private fun getQuote(symbol: String): Quote { 65 | return checkNotNull(stocksProvider.getStock(symbol)) 66 | } 67 | 68 | private fun getPosition(symbol: String): Position { 69 | return stocksProvider.getPosition(symbol) ?: Position(symbol) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/data/HistoricalData.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class HistoricalDataResult( 8 | @SerialName("chart") val chart: Chart 9 | ) 10 | 11 | @Serializable 12 | data class Result( 13 | @SerialName("meta") val meta: Meta, 14 | @SerialName("timestamp") val timestamp: List?, 15 | @SerialName("indicators") val indicators: Indicators? 16 | ) 17 | 18 | @Serializable 19 | data class Indicators( 20 | @SerialName("quote") val quote: List?, 21 | ) 22 | 23 | @Serializable 24 | data class Chart( 25 | @SerialName("result") val result: List, 26 | @SerialName("error") val error: String? 27 | ) 28 | 29 | @Serializable 30 | data class Meta( 31 | @SerialName("currency") val currency: String, 32 | @SerialName("symbol") val symbol: String, 33 | @SerialName("exchangeName") val exchangeName: String?, 34 | @SerialName("instrumentType") val instrumentType: String?, 35 | @SerialName("firstTradeDate") val firstTradeDate: Long?, 36 | @SerialName("regularMarketTime") val regularMarketTime: Long?, 37 | @SerialName("gmtoffset") val gmtoffset: Long?, 38 | @SerialName("timezone") val timezone: String?, 39 | @SerialName("exchangeTimezoneName") val exchangeTimezoneName: String?, 40 | @SerialName("regularMarketPrice") val regularMarketPrice: Double, 41 | @SerialName("chartPreviousClose") val chartPreviousClose: Double, 42 | @SerialName("priceHint") val priceHint: Long?, 43 | @SerialName("currentTradingPeriod") val currentTradingPeriod: CurrentTradingPeriod?, 44 | @SerialName("dataGranularity") val dataGranularity: String?, 45 | @SerialName("range") val range: String?, 46 | @SerialName("validRanges") val validRanges: List? 47 | ) 48 | 49 | @Serializable 50 | data class CurrentTradingPeriod( 51 | @SerialName("pre") val pre: TradingPeriod?, 52 | @SerialName("regular") val regular: TradingPeriod?, 53 | @SerialName("post") val post: TradingPeriod?, 54 | ) 55 | 56 | @Serializable 57 | data class TradingPeriod( 58 | @SerialName("timezone") val timezone: String?, 59 | @SerialName("start") val start: Long?, 60 | @SerialName("end") val end: Long?, 61 | @SerialName("gmtoffset") val gmtoffset: Long?, 62 | ) 63 | 64 | @Serializable 65 | data class DataQuote( 66 | @SerialName("close") val close: List?, 67 | @SerialName("open") val open: List?, 68 | @SerialName("low") val low: List?, 69 | @SerialName("volume") val volume: List?, 70 | @SerialName("high") val high: List?, 71 | ) 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/network/CommitsProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network 2 | 3 | import android.annotation.SuppressLint 4 | import com.github.premnirmal.ticker.model.FetchResult 5 | import com.github.premnirmal.tickerwidget.BuildConfig 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | class CommitsProvider @Inject constructor( 11 | // private val githubApi: GithubApi, 12 | // private val coroutineScope: CoroutineScope 13 | ) { 14 | // private var cachedChanges: List? = null 15 | 16 | fun initCache() { 17 | // coroutineScope.launch { fetchRepoCommits() } 18 | } 19 | 20 | // private suspend fun fetchRepoCommits(): FetchResult> { 21 | // cachedChanges?.let { return FetchResult.success(it) } 22 | // return withContext(Dispatchers.IO) { 23 | // try { 24 | // val currentVersion = BuildConfig.VERSION_NAME 25 | // val previousVersion = BuildConfig.PREVIOUS_VERSION 26 | // val comparison = githubApi.compareTags( 27 | // previousVersion, 28 | // currentVersion 29 | // ) 30 | // val commits = comparison.commits.asReversed() 31 | // cachedChanges = commits 32 | // return@withContext FetchResult.success(commits) 33 | // } catch (ex: Exception) { 34 | // Timber.w(ex) 35 | // return@withContext FetchResult.failure(ex) 36 | // } 37 | // } 38 | // } 39 | 40 | @SuppressLint("DefaultLocale") 41 | /* suspend */ fun fetchWhatsNew(): FetchResult> { 42 | // with(fetchRepoCommits()) { 43 | // return if (wasSuccessful) { 44 | // FetchResult.success( 45 | // data.filterNot { 46 | // it.commit.message.contains("Vcode++") || 47 | // it.commit.message.contains("vcode++") || 48 | // it.commit.message.contains("Merge branch 'master'") 49 | // }.map { commit -> 50 | // commit.commit.message.replace( 51 | // "\n", 52 | // "" 53 | // ).replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } 54 | // } 55 | // ) 56 | // } else { 57 | // FetchResult.failure(error) 58 | // } 59 | // } 60 | val changeLog = BuildConfig.CHANGE_LOG.split("\n") 61 | return FetchResult.success(changeLog) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/portfolio/search/SuggestionItem.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.portfolio.search 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.IconButton 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.compose.ui.text.AnnotatedString 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.dp 19 | import com.github.premnirmal.ticker.network.data.Suggestion 20 | import com.github.premnirmal.ticker.ui.Divider 21 | import com.github.premnirmal.tickerwidget.R 22 | 23 | @Composable 24 | fun SuggestionItem( 25 | modifier: Modifier = Modifier, 26 | suggestion: Suggestion, 27 | onSuggestionClick: (Suggestion) -> Unit, 28 | onSuggestionAddRemoveClick: (Suggestion) -> Unit, 29 | ) { 30 | Column( 31 | modifier = modifier 32 | ) { 33 | Row( 34 | modifier = Modifier.padding(start = 8.dp), 35 | verticalAlignment = Alignment.CenterVertically 36 | ) { 37 | Text( 38 | modifier = Modifier 39 | .weight(1f) 40 | .clickable { onSuggestionClick(suggestion) }, 41 | text = AnnotatedString(text = suggestion.displayString()), 42 | style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface), 43 | ) 44 | IconButton( 45 | onClick = { 46 | onSuggestionAddRemoveClick(suggestion) 47 | } 48 | ) { 49 | Icon( 50 | painter = painterResource( 51 | id = R.drawable.ic_add_to_list 52 | ), 53 | contentDescription = null 54 | ) 55 | } 56 | } 57 | Divider( 58 | modifier = Modifier 59 | .fillMaxWidth() 60 | .padding(top = 2.dp, end = 4.dp, start = 4.dp) 61 | ) 62 | } 63 | } 64 | 65 | @Preview 66 | @Composable 67 | fun SuggestionItemPreview() { 68 | SuggestionItem( 69 | modifier = Modifier.fillMaxWidth(), 70 | suggestion = Suggestion(symbol = "AAPL"), 71 | onSuggestionClick = {}, 72 | onSuggestionAddRemoveClick = { } 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/ModalBottomSheetWithMessage.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.ModalBottomSheet 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.dp 19 | import com.github.premnirmal.ticker.ui.AppMessage.BottomSheetMessage 20 | 21 | @Composable 22 | fun ModalBottomSheetWithMessage( 23 | message: BottomSheetMessage 24 | ) { 25 | Column( 26 | modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, bottom = 24.dp), 27 | horizontalAlignment = Alignment.Start, 28 | ) { 29 | BottomSheetHandle(modifier = Modifier.align(Alignment.CenterHorizontally)) 30 | Text( 31 | modifier = Modifier.padding(top = 8.dp, bottom = 16.dp), 32 | text = message.title, 33 | style = MaterialTheme.typography.titleLarge, 34 | ) 35 | Text( 36 | modifier = Modifier.padding(bottom = 8.dp), 37 | text = message.message, 38 | style = MaterialTheme.typography.bodyMedium, 39 | ) 40 | } 41 | } 42 | 43 | @Composable 44 | fun BottomSheetHandle(modifier: Modifier = Modifier) { 45 | Surface( 46 | modifier = modifier.padding(vertical = 8.dp), 47 | shape = MaterialTheme.shapes.extraLarge, 48 | color = MaterialTheme.colorScheme.onSurfaceVariant 49 | ) { 50 | Box( 51 | Modifier.size(width = 32.dp, height = 4.dp) 52 | ) 53 | } 54 | } 55 | 56 | @Composable 57 | @OptIn(ExperimentalMaterial3Api::class) 58 | fun BottomSheetWithMessage( 59 | message: BottomSheetMessage, 60 | onDismissRequest: () -> Unit = {}, 61 | ) { 62 | ModalBottomSheet( 63 | dragHandle = {}, 64 | onDismissRequest = onDismissRequest, 65 | ) { 66 | ModalBottomSheetWithMessage(message) 67 | } 68 | } 69 | 70 | @Preview 71 | @Composable 72 | private fun PreviewBottomSheetWithMessage() { 73 | Box(modifier = Modifier.fillMaxSize()) { 74 | BottomSheetWithMessage( 75 | message = BottomSheetMessage( 76 | title = "Title", 77 | message = "This is a sample message for the bottom sheet.", 78 | ), 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/premnirmal/ticker/network/StocksApiTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.network 2 | 3 | import com.github.premnirmal.ticker.AppPreferences 4 | import com.github.premnirmal.ticker.BaseUnitTest 5 | import com.github.premnirmal.ticker.mock.Mocker 6 | import com.github.premnirmal.ticker.network.data.YahooResponse 7 | import com.nhaarman.mockitokotlin2.any 8 | import com.nhaarman.mockitokotlin2.doThrow 9 | import com.nhaarman.mockitokotlin2.verify 10 | import com.nhaarman.mockitokotlin2.whenever 11 | import dagger.hilt.android.testing.HiltAndroidTest 12 | import kotlinx.coroutines.runBlocking 13 | import org.junit.After 14 | import org.junit.Before 15 | import org.junit.Test 16 | import retrofit2.Response 17 | 18 | @HiltAndroidTest 19 | class StocksApiTest : BaseUnitTest() { 20 | 21 | companion object { 22 | val TEST_TICKER_LIST = arrayListOf("SPY", "GOOG", "MSFT", "DIA", "AAPL") 23 | } 24 | 25 | internal lateinit var yahooFinance: YahooFinance 26 | internal lateinit var yahooFinanceCrumb: YahooFinanceCrumb 27 | internal lateinit var yahooFinanceInitialLoad: YahooFinanceInitialLoad 28 | internal lateinit var mockPrefs: AppPreferences 29 | 30 | private lateinit var stocksApi: StocksApi 31 | 32 | @Before fun initMocks() { 33 | runBlocking { 34 | yahooFinance = Mocker.provide(YahooFinance::class) 35 | mockPrefs = Mocker.provide(AppPreferences::class) 36 | yahooFinanceCrumb = Mocker.provide(YahooFinanceCrumb::class) 37 | yahooFinanceInitialLoad = Mocker.provide(YahooFinanceInitialLoad::class) 38 | val suggestionApi = Mocker.provide(SuggestionApi::class) 39 | stocksApi = StocksApi(yahooFinanceInitialLoad, yahooFinanceCrumb, yahooFinance, mockPrefs, suggestionApi) 40 | val yahooStockList = parseJsonFile("YahooQuotes.json") 41 | whenever(yahooFinance.getStocks(any())).thenReturn(Response.success(200, yahooStockList)) 42 | } 43 | } 44 | 45 | @After fun clear() { 46 | Mocker.clearMocks() 47 | } 48 | 49 | @Test fun testGetStocks() { 50 | runBlocking { 51 | val testTickerList = TEST_TICKER_LIST 52 | val stocks = stocksApi.getStocks(testTickerList) 53 | verify(yahooFinance).getStocks(any()) 54 | assertEquals(testTickerList.size, stocks.data.size) 55 | } 56 | } 57 | 58 | @Test 59 | fun testFailure() { 60 | runBlocking { 61 | val error = RuntimeException() 62 | doThrow(error).whenever(yahooFinance) 63 | .getStocks(any()) 64 | val testTickerList = TEST_TICKER_LIST 65 | val result = stocksApi.getStocks(testTickerList) 66 | assertFalse(result.wasSuccessful) 67 | assertTrue(result.hasError) 68 | verify(yahooFinance).getStocks(any()) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/ui/LinkText.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.ui 2 | 3 | import android.content.Context 4 | import androidx.compose.foundation.text.ClickableText 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.toArgb 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.text.AnnotatedString 11 | import androidx.compose.ui.text.SpanStyle 12 | import androidx.compose.ui.text.buildAnnotatedString 13 | import androidx.compose.ui.text.style.TextDecoration 14 | import androidx.compose.ui.text.withStyle 15 | import com.github.premnirmal.ticker.CustomTabs 16 | 17 | data class LinkTextData( 18 | val text: String, 19 | val tag: String? = null, 20 | val annotation: String? = null, 21 | val onClick: ((context: Context, str: AnnotatedString.Range) -> Unit)? = null, 22 | ) 23 | 24 | @Composable 25 | fun LinkText( 26 | linkTextData: List, 27 | modifier: Modifier = Modifier, 28 | ) { 29 | val annotatedString = createAnnotatedString(linkTextData) 30 | val context = LocalContext.current 31 | val primaryColor = MaterialTheme.colorScheme.primary 32 | ClickableText( 33 | text = annotatedString, 34 | style = MaterialTheme.typography.bodySmall, 35 | onClick = { offset -> 36 | linkTextData.forEach { annotatedStringData -> 37 | if (annotatedStringData.tag != null && annotatedStringData.annotation != null) { 38 | annotatedString.getStringAnnotations( 39 | tag = annotatedStringData.tag, 40 | start = offset, 41 | end = offset, 42 | ).firstOrNull()?.let { 43 | annotatedStringData.onClick?.invoke(context, it) ?: CustomTabs.openTab(context, it.item, primaryColor.toArgb()) 44 | } 45 | } 46 | } 47 | }, 48 | modifier = modifier, 49 | ) 50 | } 51 | 52 | @Composable 53 | private fun createAnnotatedString(data: List): AnnotatedString { 54 | return buildAnnotatedString { 55 | data.forEach { linkTextData -> 56 | if (linkTextData.tag != null && linkTextData.annotation != null) { 57 | pushStringAnnotation( 58 | tag = linkTextData.tag, 59 | annotation = linkTextData.annotation, 60 | ) 61 | withStyle( 62 | style = SpanStyle( 63 | color = MaterialTheme.colorScheme.primary, 64 | textDecoration = TextDecoration.Underline, 65 | ), 66 | ) { 67 | append(linkTextData.text) 68 | } 69 | pop() 70 | } else { 71 | append(linkTextData.text) 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/premnirmal/ticker/navigation/RootGraph.kt: -------------------------------------------------------------------------------- 1 | package com.github.premnirmal.ticker.navigation 2 | 3 | import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass 4 | import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.CompositionLocalProvider 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.staticCompositionLocalOf 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.lifecycle.ViewModelStoreOwner 11 | import androidx.navigation.NavHostController 12 | import androidx.navigation.compose.NavHost 13 | import androidx.navigation.compose.composable 14 | import androidx.window.layout.DisplayFeature 15 | import com.github.premnirmal.ticker.detail.QuoteDetailScreen 16 | import com.github.premnirmal.ticker.home.HomeListDetail 17 | import com.github.premnirmal.ticker.network.data.Quote 18 | 19 | @Composable 20 | fun RootNavigationGraph( 21 | windowWidthSizeClass: WindowWidthSizeClass, 22 | windowHeightSizeClass: WindowHeightSizeClass, 23 | displayFeatures: List, 24 | navHostController: NavHostController 25 | ) { 26 | val viewModelStoreOwner = rememberViewModelStoreOwner() 27 | CompositionLocalProvider(LocalNavGraphViewModelStoreOwner provides viewModelStoreOwner) { 28 | NavHost( 29 | navController = navHostController, 30 | route = Graph.ROOT, 31 | startDestination = Graph.HOME 32 | ) { 33 | composable(route = "${Graph.QUOTE_DETAIL}/{symbol}") { 34 | val symbol = it.arguments?.getString("symbol") 35 | symbol?.let { symbol -> 36 | QuoteDetailScreen( 37 | widthSizeClass = windowWidthSizeClass, 38 | contentType = null, 39 | displayFeatures = displayFeatures, 40 | quote = Quote(symbol = symbol) 41 | ) 42 | return@composable 43 | } 44 | } 45 | composable(route = Graph.HOME) { 46 | HomeListDetail( 47 | rootNavController = navHostController, 48 | windowWidthSizeClass = windowWidthSizeClass, 49 | windowHeightSizeClass = windowHeightSizeClass, 50 | displayFeatures = displayFeatures 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | 57 | @Composable 58 | private fun rememberViewModelStoreOwner(): ViewModelStoreOwner { 59 | val context = LocalContext.current 60 | return remember(context) { context as ViewModelStoreOwner } 61 | } 62 | 63 | object Graph { 64 | const val ROOT = "root_graph" 65 | const val HOME = "home_graph" 66 | const val QUOTE_DETAIL = "quote_detail_graph" 67 | } 68 | 69 | val LocalNavGraphViewModelStoreOwner = staticCompositionLocalOf { 70 | error("No LocalNavGraphViewModelStoreOwner provided") 71 | } 72 | --------------------------------------------------------------------------------