├── 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 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-en-rGB/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Text colour
4 | Text colour updated
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/premnirmal/ticker/home/IAppReviewManager.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.home
2 |
3 | import android.app.Activity
4 |
5 | interface IAppReviewManager {
6 | fun launchReviewFlow(activity: Activity) {}
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/premnirmal/ticker/model/FetchException.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.model
2 |
3 | import java.io.IOException
4 |
5 | class FetchException(message: String, ex: Throwable? = null) : IOException(message, ex)
6 |
--------------------------------------------------------------------------------
/app/src/purefoss/res/values/app_name.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Stocks Widget
4 | 1
5 |
6 |
--------------------------------------------------------------------------------
/app/src/dev/java/com/github/premnirmal/ticker/components/LoggingTree.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.components
2 |
3 | import timber.log.Timber
4 |
5 | /**
6 | * Created by premnirmal on 2/28/16.
7 | */
8 | class LoggingTree : Timber.DebugTree()
--------------------------------------------------------------------------------
/app/src/main/res/values-night/widget_colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ccff66
4 | #1d1d1d
5 | #FFFAFF
6 |
--------------------------------------------------------------------------------
/app/src/purefoss/java/com/github/premnirmal/ticker/components/LoggingTree.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.components
2 |
3 | import timber.log.Timber
4 |
5 | /**
6 | * Created by premnirmal on 2/28/16.
7 | */
8 | internal class LoggingTree : Timber.DebugTree()
--------------------------------------------------------------------------------
/app/src/main/res/drawable/transparent_widget_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/purefoss/java/com/github/premnirmal/ticker/home/AppReviewManager.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.home
2 |
3 | import android.content.Context
4 | import dagger.hilt.android.qualifiers.ApplicationContext
5 |
6 | class AppReviewManager(@ApplicationContext context: Context) : IAppReviewManager
--------------------------------------------------------------------------------
/app/src/main/res/drawable/app_widget_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | plugins {
10 | // See https://splitties.github.io/refreshVersions
11 | id("de.fayard.refreshVersions") version "0.60.5"
12 | }
13 | include(":app")
14 | include(":UI")
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_in_fast.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/premnirmal/ticker/network/ApeWisdom.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.network
2 |
3 | import com.github.premnirmal.ticker.network.data.TrendingResult
4 | import retrofit2.http.GET
5 |
6 | interface ApeWisdom {
7 |
8 | @GET("filter/stocks")
9 | suspend fun getTrendingStocks(): TrendingResult
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_out_fast.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jan 20 21:13:00 GMT 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | /projectFilesBackup/
6 | .DS_Store
7 | /build
8 | /captures
9 | app/build
10 | app/full-r8-config.txt
11 | app/prod/
12 | /.vs/
13 | .kotlin/
14 |
15 | #ignore keystore and keystore.properties
16 | *.jks
17 | keystore.*
18 | app/keystore.*
19 |
20 | #google services
21 | app/google-services.json
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v31/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v27/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_remove.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/loadview.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_trending_up.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_home.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_trending_down.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/dev/java/com/github/premnirmal/ticker/analytics/AnalyticsImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.analytics
2 |
3 | import android.content.Context
4 | import dagger.hilt.android.qualifiers.ApplicationContext
5 |
6 | /**
7 | * Created by premnirmal on 2/28/16.
8 | */
9 | class AnalyticsImpl(
10 | @ApplicationContext context: Context,
11 | generalProperties: dagger.Lazy
12 | ) : Analytics
--------------------------------------------------------------------------------
/app/src/purefoss/java/com/github/premnirmal/ticker/analytics/AnalyticsImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.analytics
2 |
3 | import android.content.Context
4 | import dagger.hilt.android.qualifiers.ApplicationContext
5 |
6 | /**
7 | * Created by premnirmal on 2/28/16.
8 | */
9 | class AnalyticsImpl(
10 | @ApplicationContext context: Context,
11 | generalProperties: dagger.Lazy
12 | ) : Analytics
--------------------------------------------------------------------------------
/app/src/main/java/com/github/premnirmal/ticker/news/NewsFeedItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.news
2 |
3 | import com.github.premnirmal.ticker.network.data.NewsArticle
4 | import com.github.premnirmal.ticker.network.data.Quote
5 |
6 | sealed class NewsFeedItem {
7 | class ArticleNewsFeed(val article: NewsArticle) : NewsFeedItem()
8 | class TrendingStockNewsFeed(val quotes: List) : NewsFeedItem()
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_done.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/app_widget_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/app_widget_background_dark.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/UI/src/main/java/com/github/premnirmal/tickerwidget/ui/theme/AppShapes.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.tickerwidget.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material3.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val AppShapes = Shapes(
8 | small = RoundedCornerShape(24.dp),
9 | medium = RoundedCornerShape(16.dp),
10 | large = RoundedCornerShape(8.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dark_widget_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
11 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_back.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_widget.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/translucent_widget_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
11 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/widget_empty_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/light_widget_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/premnirmal/ticker/network/GithubApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.premnirmal.ticker.network
2 |
3 | import com.github.premnirmal.ticker.network.data.TagComparison
4 | import retrofit2.http.GET
5 | import retrofit2.http.Path
6 |
7 | interface GithubApi {
8 |
9 | @GET("repos/premnirmal/stockticker/compare/{v1}...{v2}")
10 | suspend fun compareTags(
11 | @Path("v1") v1: String,
12 | @Path("v2") v2: String
13 | ): TagComparison
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v26/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v31/widget_themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 | [](https://github.com/premnirmal/StockTicker/actions) [](https://github.com/premnirmal/StockTicker/actions)
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 
12 | 
13 | 
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 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------