Quote preview and search enhancement (#104)
* WIP - quote preview * Adding add-remove functionality from search and from detail * add missing resources * update quote detail * vcode ++ * refresh UI * vcode++ * nav bar divider * nav bar color
This commit is contained in:
@@ -56,7 +56,7 @@
|
||||
android:theme="@style/SearchTheme"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="com.github.premnirmal.ticker.news.NewsFeedActivity"
|
||||
android:name="com.github.premnirmal.ticker.news.QuoteDetailActivity"
|
||||
android:label="@string/news_feed"
|
||||
android:theme="@style/NewsFeedActivityTheme" />
|
||||
<activity
|
||||
|
||||
@@ -19,6 +19,9 @@ abstract class BaseGraphActivity : BaseActivity() {
|
||||
protected var dataPoints: List<DataPoint>? = null
|
||||
protected lateinit var quote: Quote
|
||||
|
||||
protected val isQuoteInitialized: Boolean
|
||||
get() = this::quote.isInitialized
|
||||
|
||||
protected fun setupGraphView() {
|
||||
val graphView: LineChart = findViewById(R.id.graphView)
|
||||
graphView.isDoubleTapToZoomEnabled = false
|
||||
|
||||
@@ -20,9 +20,8 @@ import com.github.premnirmal.ticker.network.NewsProvider
|
||||
import com.github.premnirmal.ticker.network.RequestInterceptor
|
||||
import com.github.premnirmal.ticker.network.StocksApi
|
||||
import com.github.premnirmal.ticker.network.UserAgentInterceptor
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import com.github.premnirmal.ticker.news.GraphActivity
|
||||
import com.github.premnirmal.ticker.news.NewsFeedActivity
|
||||
import com.github.premnirmal.ticker.news.QuoteDetailActivity
|
||||
import com.github.premnirmal.ticker.portfolio.AddPositionActivity
|
||||
import com.github.premnirmal.ticker.portfolio.PortfolioFragment
|
||||
import com.github.premnirmal.ticker.portfolio.StocksAdapter
|
||||
@@ -55,7 +54,7 @@ interface AppComponent {
|
||||
|
||||
fun inject(splashActivity: SplashActivity)
|
||||
|
||||
fun inject(newsFeedActivity: NewsFeedActivity)
|
||||
fun inject(newsFeedActivity: QuoteDetailActivity)
|
||||
|
||||
fun inject(graphActivity: GraphActivity)
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ class HomeFragment : BaseFragment(), ChildFragment, PortfolioFragment.Parent {
|
||||
|
||||
private fun updateHeader() {
|
||||
tabs.visibility = if (widgetDataProvider.hasWidget()) View.VISIBLE else View.INVISIBLE
|
||||
adapter.notifyDataSetChanged()
|
||||
subtitle.text = subtitleText
|
||||
if (!widgetDataProvider.hasWidget()) {
|
||||
fab_settings.show()
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.premnirmal.ticker.model
|
||||
import com.github.premnirmal.ticker.network.data.Holding
|
||||
import com.github.premnirmal.ticker.network.data.Position
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
||||
/**
|
||||
* Created by premnirmal on 2/28/16.
|
||||
@@ -29,6 +30,8 @@ interface IStocksProvider {
|
||||
|
||||
fun getStock(ticker: String): Quote?
|
||||
|
||||
suspend fun fetchStock(ticker: String): Quote?
|
||||
|
||||
fun removeStock(ticker: String): Collection<String>
|
||||
|
||||
fun removeStocks(tickers: Collection<String>)
|
||||
|
||||
@@ -17,7 +17,9 @@ import com.github.premnirmal.ticker.network.data.Position
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import com.github.premnirmal.ticker.widget.WidgetDataProvider
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.threeten.bp.DayOfWeek
|
||||
@@ -128,8 +130,8 @@ class StocksProvider : IStocksProvider {
|
||||
get() = alarmScheduler.msToNextAlarm(lastFetched)
|
||||
|
||||
private fun scheduleUpdateWithMs(
|
||||
msToNextAlarm: Long,
|
||||
refresh: Boolean = false
|
||||
msToNextAlarm: Long,
|
||||
refresh: Boolean = false
|
||||
) {
|
||||
val updateTime = alarmScheduler.scheduleUpdate(msToNextAlarm, context)
|
||||
nextFetch = updateTime.toInstant()
|
||||
@@ -173,7 +175,9 @@ class StocksProvider : IStocksProvider {
|
||||
override suspend fun fetch(): FetchResult<List<Quote>> = withContext(Dispatchers.IO) {
|
||||
if (tickerList.isEmpty()) {
|
||||
bus.send(ErrorEvent(context.getString(R.string.no_symbols_in_portfolio)))
|
||||
return@withContext FetchResult<List<Quote>>(_error = FetchException("No symbols in portfolio"))
|
||||
return@withContext FetchResult<List<Quote>>(
|
||||
_error = FetchException("No symbols in portfolio")
|
||||
)
|
||||
} else {
|
||||
val result = try {
|
||||
appPreferences.setRefreshing(true)
|
||||
@@ -227,6 +231,7 @@ class StocksProvider : IStocksProvider {
|
||||
quote.symbol = ticker
|
||||
quoteList[ticker] = quote
|
||||
save()
|
||||
bus.send(RefreshEvent())
|
||||
ApplicationScope.launch {
|
||||
fetch()
|
||||
}
|
||||
@@ -240,12 +245,12 @@ class StocksProvider : IStocksProvider {
|
||||
override fun getPosition(ticker: String): Position? = positionList[ticker]
|
||||
|
||||
override fun addPosition(
|
||||
ticker: String,
|
||||
shares: Float,
|
||||
price: Float
|
||||
ticker: String,
|
||||
shares: Float,
|
||||
price: Float
|
||||
): Holding {
|
||||
synchronized(quoteList) {
|
||||
val quote = getStock(ticker)
|
||||
val quote = quoteList[ticker]
|
||||
var position = getPosition(ticker)
|
||||
if (position == null) {
|
||||
position = Position(ticker, ArrayList())
|
||||
@@ -263,12 +268,12 @@ class StocksProvider : IStocksProvider {
|
||||
}
|
||||
|
||||
override fun removePosition(
|
||||
ticker: String,
|
||||
holding: Holding
|
||||
ticker: String,
|
||||
holding: Holding
|
||||
) {
|
||||
synchronized(positionList) {
|
||||
val position = getPosition(ticker)
|
||||
val quote = getStock(ticker)
|
||||
val quote = quoteList[ticker]
|
||||
position?.remove(holding)
|
||||
quote?.position = position
|
||||
save()
|
||||
@@ -295,7 +300,7 @@ class StocksProvider : IStocksProvider {
|
||||
quoteList.remove(ticker)
|
||||
positionList.remove(ticker)
|
||||
save()
|
||||
scheduleUpdate()
|
||||
scheduleUpdate(true)
|
||||
return tickerList
|
||||
}
|
||||
}
|
||||
@@ -308,7 +313,13 @@ class StocksProvider : IStocksProvider {
|
||||
positionList.remove(it)
|
||||
}
|
||||
save()
|
||||
scheduleUpdate()
|
||||
scheduleUpdate(true)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchStock(ticker: String): Quote? = withContext(Dispatchers.IO) {
|
||||
return@withContext quoteList[ticker] ?: run {
|
||||
api.getStock(ticker)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.github.premnirmal.ticker.model.FetchException
|
||||
import com.github.premnirmal.ticker.model.FetchResult
|
||||
import com.github.premnirmal.ticker.network.data.IQuoteNet
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import com.github.premnirmal.ticker.network.data.Suggestions.Suggestion
|
||||
import com.github.premnirmal.ticker.network.data.SuggestionsNet.SuggestionNet
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -36,13 +36,13 @@ class StocksApi {
|
||||
suggestionApi.getSuggestions(query)
|
||||
.resultSet?.result
|
||||
} catch (e: Exception) {
|
||||
return@withContext FetchResult<List<Suggestion>>(_error = FetchException("Error fetching", e))
|
||||
return@withContext FetchResult<List<SuggestionNet>>(_error = FetchException("Error fetching", e))
|
||||
}
|
||||
val suggestionList = suggestions?.let { ArrayList(it) } ?: ArrayList()
|
||||
if (suggestionList.isEmpty()) {
|
||||
suggestionList.add(0, Suggestion(query))
|
||||
suggestionList.add(0, SuggestionNet(query))
|
||||
}
|
||||
return@withContext FetchResult<List<Suggestion>>(_data = suggestionList)
|
||||
return@withContext FetchResult<List<SuggestionNet>>(_data = suggestionList)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +61,15 @@ class StocksApi {
|
||||
.toOrderedQuoteList(tickerList)
|
||||
}
|
||||
|
||||
suspend fun getStock(ticker: String) = withContext(Dispatchers.IO) {
|
||||
val quoteNets = try {
|
||||
robindahood.getStocks(ticker)
|
||||
} catch (ex: HttpException) {
|
||||
getStocksYahoo(listOf(ticker))
|
||||
}
|
||||
return@withContext Quote.fromQuoteNet(quoteNets.first())
|
||||
}
|
||||
|
||||
private suspend fun getStocksYahoo(tickerList: List<String>) = withContext(Dispatchers.IO) {
|
||||
val query = tickerList.joinToString(",")
|
||||
val quoteNets = yahooFinance.getStocks(query).quoteResponse!!.result
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
package com.github.premnirmal.ticker.network
|
||||
|
||||
import com.github.premnirmal.ticker.network.data.Suggestions
|
||||
import com.github.premnirmal.ticker.network.data.SuggestionsNet
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.IOException
|
||||
@@ -9,14 +9,14 @@ import java.util.regex.Pattern
|
||||
/**
|
||||
* Created by premnirmal on 3/3/16.
|
||||
*/
|
||||
internal class StupidYahooWrapConverter(gson: Gson) : BaseConverter<Suggestions>(gson) {
|
||||
internal class StupidYahooWrapConverter(gson: Gson) : BaseConverter<SuggestionsNet>(gson) {
|
||||
|
||||
override fun convert(value: ResponseBody?): Suggestions? {
|
||||
override fun convert(value: ResponseBody?): SuggestionsNet? {
|
||||
try {
|
||||
val bodyString = value?.let { getString(it.byteStream()) }
|
||||
val m = PATTERN_RESPONSE.matcher(bodyString)
|
||||
if (m.find()) {
|
||||
return gson.fromJson(m.group(1), Suggestions::class.java)
|
||||
return gson.fromJson(m.group(1), SuggestionsNet::class.java)
|
||||
}
|
||||
throw error("Invalid response")
|
||||
} catch (e: IOException) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.github.premnirmal.ticker.network
|
||||
|
||||
import com.github.premnirmal.ticker.network.data.Suggestions
|
||||
import com.github.premnirmal.ticker.network.data.SuggestionsNet
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
@@ -10,6 +10,6 @@ import retrofit2.http.Query
|
||||
interface SuggestionApi {
|
||||
|
||||
@GET("autoc?callback=YAHOO.Finance.SymbolSuggest.ssCallback®ion=US&lang=en-US")
|
||||
suspend fun getSuggestions(@Query("query") query: String): Suggestions
|
||||
suspend fun getSuggestions(@Query("query") query: String): SuggestionsNet
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.github.premnirmal.ticker.network.data
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.os.Parcelable.Creator
|
||||
import com.github.premnirmal.ticker.AppPreferences
|
||||
import java.text.Format
|
||||
|
||||
/**
|
||||
* Created by premnirmal on 3/30/17.
|
||||
*/
|
||||
data class Quote(var symbol: String = "") : Comparable<Quote> {
|
||||
data class Quote(var symbol: String = "") : Parcelable, Comparable<Quote> {
|
||||
|
||||
companion object {
|
||||
fun fromQuoteNet(quoteNet: IQuoteNet): Quote {
|
||||
@@ -20,6 +23,17 @@ data class Quote(var symbol: String = "") : Comparable<Quote> {
|
||||
quote.description = quoteNet.description ?: ""
|
||||
return quote
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val CREATOR = object: Creator<Quote> {
|
||||
override fun createFromParcel(parcel: Parcel): Quote {
|
||||
return Quote(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Quote?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var name: String = ""
|
||||
@@ -129,4 +143,38 @@ data class Quote(var symbol: String = "") : Comparable<Quote> {
|
||||
|
||||
override operator fun compareTo(other: Quote): Int =
|
||||
other.changeInPercent.compareTo(changeInPercent)
|
||||
|
||||
constructor(parcel: Parcel) : this(parcel.readString()!!) {
|
||||
name = parcel.readString()!!
|
||||
lastTradePrice = parcel.readFloat()
|
||||
changeInPercent = parcel.readFloat()
|
||||
change = parcel.readFloat()
|
||||
stockExchange = parcel.readString()!!
|
||||
currency = parcel.readString()!!
|
||||
description = parcel.readString()!!
|
||||
isPosition = parcel.readByte() != 0.toByte()
|
||||
positionPrice = parcel.readFloat()
|
||||
positionShares = parcel.readFloat()
|
||||
}
|
||||
|
||||
override fun writeToParcel(
|
||||
parcel: Parcel,
|
||||
flags: Int
|
||||
) {
|
||||
parcel.writeString(symbol)
|
||||
parcel.writeString(name)
|
||||
parcel.writeFloat(lastTradePrice)
|
||||
parcel.writeFloat(changeInPercent)
|
||||
parcel.writeFloat(change)
|
||||
parcel.writeString(stockExchange)
|
||||
parcel.writeString(currency)
|
||||
parcel.writeString(description)
|
||||
parcel.writeByte(if (isPosition) 1 else 0)
|
||||
parcel.writeFloat(positionPrice)
|
||||
parcel.writeFloat(positionShares)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.github.premnirmal.ticker.network.data
|
||||
|
||||
import com.github.premnirmal.ticker.network.data.SuggestionsNet.SuggestionNet
|
||||
|
||||
data class Suggestion(
|
||||
var symbol: String = ""
|
||||
) {
|
||||
var name: String = ""
|
||||
var exch: String = ""
|
||||
var type: String = ""
|
||||
var exchDisp: String = ""
|
||||
var typeDisp: String = ""
|
||||
var exists: Boolean = false
|
||||
|
||||
companion object {
|
||||
fun fromSuggestionNet(suggestionNet: SuggestionNet): Suggestion {
|
||||
val suggestion = Suggestion(suggestionNet.symbol)
|
||||
suggestion.name = suggestionNet.name
|
||||
suggestion.exch = suggestionNet.exch
|
||||
suggestion.type = suggestionNet.type
|
||||
suggestion.exchDisp = suggestionNet.exchDisp
|
||||
suggestion.typeDisp = suggestionNet.typeDisp
|
||||
return suggestion
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -5,14 +5,14 @@ import com.google.gson.annotations.SerializedName
|
||||
/**
|
||||
* Created by premnirmal on 3/30/17.
|
||||
*/
|
||||
data class Suggestions(@SerializedName("ResultSet") var resultSet: QueryResults? = null) {
|
||||
data class SuggestionsNet(@SerializedName("ResultSet") var resultSet: QueryResultsNet? = null) {
|
||||
|
||||
data class QueryResults(
|
||||
data class QueryResultsNet(
|
||||
@SerializedName("Query") var query: String = "") {
|
||||
@SerializedName("Result") var result: List<Suggestion>? = null
|
||||
@SerializedName("Result") var result: List<SuggestionNet>? = null
|
||||
}
|
||||
|
||||
data class Suggestion(
|
||||
data class SuggestionNet(
|
||||
@SerializedName("symbol") var symbol: String = ""
|
||||
) {
|
||||
@SerializedName("name") var name: String = ""
|
||||
@@ -1,279 +0,0 @@
|
||||
package com.github.premnirmal.ticker.news
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.mikephil.charting.charts.LineChart
|
||||
import com.github.premnirmal.ticker.BrowserFallback
|
||||
import com.github.premnirmal.ticker.analytics.ClickEvent
|
||||
import com.github.premnirmal.ticker.analytics.GeneralEvent
|
||||
import com.github.premnirmal.ticker.base.BaseGraphActivity
|
||||
import com.github.premnirmal.ticker.components.InAppMessage
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.isNetworkOnline
|
||||
import com.github.premnirmal.ticker.model.IHistoryProvider
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.ticker.network.NewsProvider
|
||||
import com.github.premnirmal.ticker.network.data.NewsArticle
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import com.github.premnirmal.ticker.portfolio.AddPositionActivity
|
||||
import com.github.premnirmal.ticker.toBitmap
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.average_price
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.change
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.day_change
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.description
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.edit_positions
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.equityValue
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.exchange
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.graphView
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.graph_container
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.lastTradePrice
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.news_container
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.numShares
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.progress
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.tickerName
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.total_gain_loss
|
||||
import kotlinx.coroutines.launch
|
||||
import saschpe.android.customtabs.CustomTabsHelper
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewsFeedActivity : BaseGraphActivity() {
|
||||
|
||||
companion object {
|
||||
const val TICKER = "TICKER"
|
||||
private const val DATA_POINTS = "DATA_POINTS"
|
||||
}
|
||||
|
||||
@Inject internal lateinit var stocksProvider: IStocksProvider
|
||||
@Inject internal lateinit var newsProvider: NewsProvider
|
||||
@Inject internal lateinit var historyProvider: IHistoryProvider
|
||||
private lateinit var ticker: String
|
||||
override val simpleName: String = "NewsFeedActivity"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Injector.appComponent.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_news_feed)
|
||||
toolbar.setNavigationOnClickListener {
|
||||
finish()
|
||||
}
|
||||
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
graph_container.layoutParams.height = (resources.displayMetrics.widthPixels * 0.5625f).toInt()
|
||||
graph_container.requestLayout()
|
||||
}
|
||||
setupGraphView()
|
||||
val q: Quote?
|
||||
if (intent.hasExtra(TICKER) && intent.getStringExtra(TICKER) != null) {
|
||||
ticker = intent.getStringExtra(TICKER)
|
||||
q = stocksProvider.getStock(ticker)
|
||||
if (q == null) {
|
||||
showErrorAndFinish()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ticker = ""
|
||||
showErrorAndFinish()
|
||||
return
|
||||
}
|
||||
quote = q
|
||||
toolbar.title = ticker
|
||||
tickerName.text = quote.name
|
||||
lastTradePrice.text = quote.priceString()
|
||||
val changeText = "${quote.changeStringWithSign()} ( ${quote.changePercentStringWithSign()})"
|
||||
change.text = changeText
|
||||
if (quote.change > 0 || quote.changeInPercent >= 0) {
|
||||
change.setTextColor(resources.getColor(R.color.positive_green))
|
||||
lastTradePrice.setTextColor(resources.getColor(R.color.positive_green))
|
||||
} else {
|
||||
change.setTextColor(resources.getColor(R.color.negative_red))
|
||||
lastTradePrice.setTextColor(resources.getColor(R.color.negative_red))
|
||||
}
|
||||
exchange.text = quote.stockExchange
|
||||
numShares.text = quote.numSharesString()
|
||||
equityValue.text = quote.holdingsString()
|
||||
description.text = quote.description
|
||||
edit_positions.setOnClickListener {
|
||||
analytics.trackClickEvent(ClickEvent("EditPositionClick")
|
||||
.addProperty("Instrument", ticker))
|
||||
val intent = Intent(this, AddPositionActivity::class.java)
|
||||
intent.putExtra(AddPositionActivity.TICKER, quote.symbol)
|
||||
startActivity(intent)
|
||||
}
|
||||
savedInstanceState?.let {
|
||||
dataPoints = it.getParcelableArrayList(DATA_POINTS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchData() {
|
||||
if (isNetworkOnline()) {
|
||||
lifecycleScope.launch {
|
||||
val result = historyProvider.getHistoricalDataShort(quote.symbol)
|
||||
if (result.wasSuccessful) {
|
||||
dataPoints = result.data
|
||||
loadGraph()
|
||||
} else {
|
||||
progress.visibility = View.GONE
|
||||
graphView.setNoDataText(getString(R.string.graph_fetch_failed))
|
||||
InAppMessage.showMessage(this@NewsFeedActivity, R.string.graph_fetch_failed, error = true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
progress.visibility = View.GONE
|
||||
graphView.setNoDataText(getString(R.string.no_network_message))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
dataPoints?.let {
|
||||
outState.putParcelableArrayList(DATA_POINTS, ArrayList(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpArticles(articles: List<NewsArticle>) {
|
||||
if (articles.isEmpty()) {
|
||||
news_container.visibility = View.GONE
|
||||
} else {
|
||||
news_container.visibility = View.VISIBLE
|
||||
for (newsArticle in articles) {
|
||||
val layout = LayoutInflater.from(this)
|
||||
.inflate(R.layout.item_news, news_container, false)
|
||||
val sourceView: TextView = layout.findViewById(R.id.news_source)
|
||||
val titleView: TextView = layout.findViewById(R.id.news_title)
|
||||
val subTitleView: TextView = layout.findViewById(R.id.news_subtitle)
|
||||
val dateView: TextView = layout.findViewById(R.id.published_at)
|
||||
titleView.text = newsArticle.title
|
||||
subTitleView.text = newsArticle.description
|
||||
dateView.text = newsArticle.dateString()
|
||||
sourceView.text = newsArticle.sourceName()
|
||||
val params = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
params.bottomMargin = resources.getDimensionPixelSize(R.dimen.activity_vertical_margin)
|
||||
news_container.addView(layout, params)
|
||||
layout.tag = newsArticle
|
||||
layout.setOnClickListener {
|
||||
val article = it.tag as NewsArticle
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
.addDefaultShareMenuItem()
|
||||
.setToolbarColor(this.resources.getColor(R.color.colorPrimary))
|
||||
.setShowTitle(true)
|
||||
.setCloseButtonIcon(resources.getDrawable(R.drawable.ic_close).toBitmap())
|
||||
.build()
|
||||
analytics.trackClickEvent(ClickEvent("ArticleClick")
|
||||
.addProperty("Instrument", ticker)
|
||||
.addProperty("ArticleTitle", newsArticle.title.orEmpty())
|
||||
.addProperty("ArticleSource", newsArticle.sourceName())
|
||||
.addProperty("ArticleUrl", newsArticle.url.orEmpty()))
|
||||
CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent)
|
||||
CustomTabsHelper.openCustomTab(
|
||||
this, customTabsIntent, Uri.parse(article.url.orEmpty()),
|
||||
BrowserFallback()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
fetch()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
numShares.text = quote.numSharesString()
|
||||
equityValue.text = quote.holdingsString()
|
||||
if (quote.hasPositions()) {
|
||||
total_gain_loss.visibility = View.VISIBLE
|
||||
total_gain_loss.setText("${quote.gainLossString()} (${quote.gainLossPercentString()})")
|
||||
if (quote.gainLoss() >= 0) {
|
||||
total_gain_loss.setTextColor(resources.getColor(R.color.positive_green))
|
||||
} else {
|
||||
total_gain_loss.setTextColor(resources.getColor(R.color.negative_red))
|
||||
}
|
||||
average_price.visibility = View.VISIBLE
|
||||
average_price.setText(quote.averagePositionPrice())
|
||||
day_change.visibility = View.VISIBLE
|
||||
day_change.setText(quote.dayChangeString())
|
||||
if (quote.change > 0 || quote.changeInPercent >= 0) {
|
||||
day_change.setTextColor(resources.getColor(R.color.positive_green))
|
||||
} else {
|
||||
day_change.setTextColor(resources.getColor(R.color.negative_red))
|
||||
}
|
||||
} else {
|
||||
total_gain_loss.visibility = View.GONE
|
||||
day_change.visibility = View.GONE
|
||||
average_price.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetch() {
|
||||
if (!isNetworkOnline()) {
|
||||
InAppMessage.showMessage(this, R.string.no_network_message, error = true)
|
||||
}
|
||||
if (news_container.childCount <= 1) {
|
||||
fetchNews()
|
||||
}
|
||||
if (dataPoints == null) {
|
||||
fetchData()
|
||||
} else {
|
||||
loadGraph()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchNews() {
|
||||
if (isNetworkOnline()) {
|
||||
lifecycleScope.launch {
|
||||
val result = newsProvider.getNews(quote.newsQuery())
|
||||
if (result.wasSuccessful) {
|
||||
val articles = result.data
|
||||
analytics.trackGeneralEvent(GeneralEvent("FetchNews")
|
||||
.addProperty("Instrument", ticker)
|
||||
.addProperty("Success", "True"))
|
||||
setUpArticles(articles)
|
||||
} else {
|
||||
news_container.visibility = View.GONE
|
||||
InAppMessage.showMessage(this@NewsFeedActivity, R.string.news_fetch_failed, error = true)
|
||||
analytics.trackGeneralEvent(GeneralEvent("FetchNews")
|
||||
.addProperty("Instrument", ticker)
|
||||
.addProperty("Success", "False"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
news_container.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGraphDataAdded(graphView: LineChart) {
|
||||
progress.visibility = View.GONE
|
||||
analytics.trackGeneralEvent(GeneralEvent("GraphLoaded"))
|
||||
}
|
||||
|
||||
override fun onNoGraphData(graphView: LineChart) {
|
||||
progress.visibility = View.GONE
|
||||
analytics.trackGeneralEvent(GeneralEvent("NoGraphData"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called via xml
|
||||
*/
|
||||
fun openGraph(v: View) {
|
||||
analytics.trackClickEvent(ClickEvent("GraphClick")
|
||||
.addProperty("Instrument", ticker))
|
||||
val intent = Intent(this, GraphActivity::class.java)
|
||||
intent.putExtra(GraphActivity.TICKER, ticker)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
package com.github.premnirmal.ticker.news
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.mikephil.charting.charts.LineChart
|
||||
import com.github.premnirmal.ticker.BrowserFallback
|
||||
import com.github.premnirmal.ticker.analytics.ClickEvent
|
||||
import com.github.premnirmal.ticker.analytics.GeneralEvent
|
||||
import com.github.premnirmal.ticker.base.BaseGraphActivity
|
||||
import com.github.premnirmal.ticker.components.InAppMessage
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.isNetworkOnline
|
||||
import com.github.premnirmal.ticker.model.IHistoryProvider
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.ticker.network.NewsProvider
|
||||
import com.github.premnirmal.ticker.network.data.NewsArticle
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import com.github.premnirmal.ticker.portfolio.AddPositionActivity
|
||||
import com.github.premnirmal.ticker.showDialog
|
||||
import com.github.premnirmal.ticker.toBitmap
|
||||
import com.github.premnirmal.ticker.widget.WidgetDataProvider
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
import com.github.premnirmal.tickerwidget.R.color
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.average_price
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.change
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.day_change
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.description
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.edit_positions
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.equityValue
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.exchange
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.graphView
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.graph_container
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.lastTradePrice
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.news_container
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.numShares
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.positions_container
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.positions_header
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.progress
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.tickerName
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_quote_detail.total_gain_loss
|
||||
import kotlinx.coroutines.launch
|
||||
import saschpe.android.customtabs.CustomTabsHelper
|
||||
import javax.inject.Inject
|
||||
|
||||
class QuoteDetailActivity : BaseGraphActivity() {
|
||||
|
||||
companion object {
|
||||
const val TICKER = "TICKER"
|
||||
private const val DATA_POINTS = "DATA_POINTS"
|
||||
private const val QUOTE = "QUOTE"
|
||||
}
|
||||
|
||||
@Inject internal lateinit var stocksProvider: IStocksProvider
|
||||
@Inject internal lateinit var newsProvider: NewsProvider
|
||||
@Inject internal lateinit var historyProvider: IHistoryProvider
|
||||
@Inject internal lateinit var widgetDataProvider: WidgetDataProvider
|
||||
private lateinit var ticker: String
|
||||
override val simpleName: String = "NewsFeedActivity"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Injector.appComponent.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_quote_detail)
|
||||
toolbar.setNavigationOnClickListener {
|
||||
finish()
|
||||
}
|
||||
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
graph_container.layoutParams.height = (resources.displayMetrics.widthPixels * 0.5625f).toInt()
|
||||
graph_container.requestLayout()
|
||||
}
|
||||
setupGraphView()
|
||||
savedInstanceState?.let {
|
||||
ticker = intent.getStringExtra(TICKER)
|
||||
dataPoints = it.getParcelableArrayList(DATA_POINTS)
|
||||
quote = checkNotNull(it.getParcelable(QUOTE))
|
||||
fetch()
|
||||
setupUi()
|
||||
} ?: run {
|
||||
lifecycleScope.launch {
|
||||
val q: Quote?
|
||||
if (intent.hasExtra(TICKER) && intent.getStringExtra(TICKER) != null) {
|
||||
ticker = intent.getStringExtra(TICKER)
|
||||
q = stocksProvider.fetchStock(ticker)
|
||||
if (q == null) {
|
||||
showErrorAndFinish()
|
||||
return@launch
|
||||
}
|
||||
} else {
|
||||
ticker = ""
|
||||
showErrorAndFinish()
|
||||
return@launch
|
||||
}
|
||||
quote = q
|
||||
fetch()
|
||||
setupUi()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
toolbar.inflateMenu(R.menu.menu_news_feed)
|
||||
val isInPortfolio = stocksProvider.hasTicker(ticker)
|
||||
val addMenuItem = toolbar.menu.findItem(R.id.action_add)
|
||||
val removeMenuItem = toolbar.menu.findItem(R.id.action_remove)
|
||||
if (isInPortfolio) {
|
||||
addMenuItem.isVisible = false
|
||||
removeMenuItem.isVisible = true
|
||||
} else {
|
||||
removeMenuItem.isVisible = false
|
||||
addMenuItem.isVisible = true
|
||||
}
|
||||
toolbar.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_add -> {
|
||||
if (widgetDataProvider.hasWidget()) {
|
||||
val widgetIds = widgetDataProvider.getAppWidgetIds()
|
||||
if (widgetIds.size > 1) {
|
||||
val widgets =
|
||||
widgetIds.map { widgetDataProvider.dataForWidgetId(it) }
|
||||
.sortedBy { it.widgetName() }
|
||||
val widgetNames = widgets.map { it.widgetName() }
|
||||
.toTypedArray()
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.select_widget)
|
||||
.setItems(widgetNames) { dialog, which ->
|
||||
val id = widgets[which].widgetId
|
||||
addTickerToWidget(ticker, id)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
} else {
|
||||
addTickerToWidget(ticker, widgetIds.first())
|
||||
}
|
||||
} else {
|
||||
addTickerToWidget(ticker, WidgetDataProvider.INVALID_WIDGET_ID)
|
||||
}
|
||||
updatePositionsUi()
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.action_remove -> {
|
||||
removeMenuItem.isVisible = false
|
||||
addMenuItem.isVisible = true
|
||||
stocksProvider.removeStock(ticker)
|
||||
updatePositionsUi()
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
}
|
||||
return@setOnMenuItemClickListener false
|
||||
}
|
||||
toolbar.title = ticker
|
||||
tickerName.text = quote.name
|
||||
lastTradePrice.text = quote.priceString()
|
||||
val changeText = "${quote.changeStringWithSign()} ( ${quote.changePercentStringWithSign()})"
|
||||
change.text = changeText
|
||||
if (quote.change > 0 || quote.changeInPercent >= 0) {
|
||||
change.setTextColor(resources.getColor(color.positive_green))
|
||||
lastTradePrice.setTextColor(resources.getColor(color.positive_green))
|
||||
} else {
|
||||
change.setTextColor(resources.getColor(color.negative_red))
|
||||
lastTradePrice.setTextColor(resources.getColor(color.negative_red))
|
||||
}
|
||||
exchange.text = quote.stockExchange
|
||||
updatePositionsUi()
|
||||
edit_positions.setOnClickListener {
|
||||
analytics.trackClickEvent(
|
||||
ClickEvent("EditPositionClick")
|
||||
.addProperty("Instrument", ticker)
|
||||
)
|
||||
val intent = Intent(this, AddPositionActivity::class.java)
|
||||
intent.putExtra(AddPositionActivity.TICKER, quote.symbol)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchData() {
|
||||
if (isNetworkOnline()) {
|
||||
lifecycleScope.launch {
|
||||
val result = historyProvider.getHistoricalDataShort(quote.symbol)
|
||||
if (result.wasSuccessful) {
|
||||
dataPoints = result.data
|
||||
loadGraph()
|
||||
} else {
|
||||
progress.visibility = View.GONE
|
||||
graphView.setNoDataText(getString(R.string.graph_fetch_failed))
|
||||
InAppMessage.showMessage(this@QuoteDetailActivity, R.string.graph_fetch_failed, error = true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
progress.visibility = View.GONE
|
||||
graphView.setNoDataText(getString(R.string.no_network_message))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
dataPoints?.let {
|
||||
outState.putParcelableArrayList(DATA_POINTS, ArrayList(it))
|
||||
}
|
||||
if (isQuoteInitialized) {
|
||||
outState.putParcelable(QUOTE, quote)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpArticles(articles: List<NewsArticle>) {
|
||||
if (articles.isEmpty()) {
|
||||
news_container.visibility = View.GONE
|
||||
} else {
|
||||
news_container.visibility = View.VISIBLE
|
||||
for (newsArticle in articles) {
|
||||
val layout = LayoutInflater.from(this)
|
||||
.inflate(R.layout.item_news, news_container, false)
|
||||
val sourceView: TextView = layout.findViewById(R.id.news_source)
|
||||
val titleView: TextView = layout.findViewById(R.id.news_title)
|
||||
val subTitleView: TextView = layout.findViewById(R.id.news_subtitle)
|
||||
val dateView: TextView = layout.findViewById(R.id.published_at)
|
||||
titleView.text = newsArticle.title
|
||||
subTitleView.text = newsArticle.description
|
||||
dateView.text = newsArticle.dateString()
|
||||
sourceView.text = newsArticle.sourceName()
|
||||
val params = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
params.bottomMargin = resources.getDimensionPixelSize(R.dimen.activity_vertical_margin)
|
||||
news_container.addView(layout, params)
|
||||
layout.tag = newsArticle
|
||||
layout.setOnClickListener {
|
||||
val article = it.tag as NewsArticle
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
.addDefaultShareMenuItem()
|
||||
.setToolbarColor(this.resources.getColor(R.color.colorPrimary))
|
||||
.setShowTitle(true)
|
||||
.setCloseButtonIcon(resources.getDrawable(R.drawable.ic_close).toBitmap())
|
||||
.build()
|
||||
analytics.trackClickEvent(
|
||||
ClickEvent("ArticleClick")
|
||||
.addProperty("Instrument", ticker)
|
||||
.addProperty("ArticleTitle", newsArticle.title.orEmpty())
|
||||
.addProperty("ArticleSource", newsArticle.sourceName())
|
||||
.addProperty("ArticleUrl", newsArticle.url.orEmpty())
|
||||
)
|
||||
CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent)
|
||||
CustomTabsHelper.openCustomTab(
|
||||
this, customTabsIntent, Uri.parse(article.url.orEmpty()),
|
||||
BrowserFallback()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (isQuoteInitialized) {
|
||||
updatePositionsUi()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePositionsUi() {
|
||||
val isInPortfolio = stocksProvider.hasTicker(ticker)
|
||||
if (isInPortfolio) {
|
||||
positions_container.visibility = View.VISIBLE
|
||||
positions_header.visibility = View.VISIBLE
|
||||
numShares.text = quote.numSharesString()
|
||||
equityValue.text = quote.holdingsString()
|
||||
description.text = quote.description
|
||||
if (quote.hasPositions()) {
|
||||
total_gain_loss.visibility = View.VISIBLE
|
||||
total_gain_loss.setText("${quote.gainLossString()} (${quote.gainLossPercentString()})")
|
||||
if (quote.gainLoss() >= 0) {
|
||||
total_gain_loss.setTextColor(resources.getColor(color.positive_green))
|
||||
} else {
|
||||
total_gain_loss.setTextColor(resources.getColor(color.negative_red))
|
||||
}
|
||||
average_price.visibility = View.VISIBLE
|
||||
average_price.setText(quote.averagePositionPrice())
|
||||
day_change.visibility = View.VISIBLE
|
||||
day_change.setText(quote.dayChangeString())
|
||||
if (quote.change > 0 || quote.changeInPercent >= 0) {
|
||||
day_change.setTextColor(resources.getColor(color.positive_green))
|
||||
} else {
|
||||
day_change.setTextColor(resources.getColor(color.negative_red))
|
||||
}
|
||||
} else {
|
||||
total_gain_loss.visibility = View.GONE
|
||||
day_change.visibility = View.GONE
|
||||
average_price.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
positions_container.visibility = View.GONE
|
||||
positions_header.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetch() {
|
||||
if (!isNetworkOnline()) {
|
||||
InAppMessage.showMessage(this, R.string.no_network_message, error = true)
|
||||
}
|
||||
if (news_container.childCount <= 1) {
|
||||
fetchNews()
|
||||
}
|
||||
if (dataPoints == null) {
|
||||
fetchData()
|
||||
} else {
|
||||
loadGraph()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchNews() {
|
||||
if (isNetworkOnline()) {
|
||||
lifecycleScope.launch {
|
||||
val result = newsProvider.getNews(quote.newsQuery())
|
||||
if (result.wasSuccessful) {
|
||||
val articles = result.data
|
||||
analytics.trackGeneralEvent(
|
||||
GeneralEvent("FetchNews")
|
||||
.addProperty("Instrument", ticker)
|
||||
.addProperty("Success", "True")
|
||||
)
|
||||
setUpArticles(articles)
|
||||
} else {
|
||||
news_container.visibility = View.GONE
|
||||
InAppMessage.showMessage(this@QuoteDetailActivity, R.string.news_fetch_failed, error = true)
|
||||
analytics.trackGeneralEvent(
|
||||
GeneralEvent("FetchNews")
|
||||
.addProperty("Instrument", ticker)
|
||||
.addProperty("Success", "False")
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
news_container.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGraphDataAdded(graphView: LineChart) {
|
||||
progress.visibility = View.GONE
|
||||
analytics.trackGeneralEvent(GeneralEvent("GraphLoaded"))
|
||||
}
|
||||
|
||||
override fun onNoGraphData(graphView: LineChart) {
|
||||
progress.visibility = View.GONE
|
||||
analytics.trackGeneralEvent(GeneralEvent("NoGraphData"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called via xml
|
||||
*/
|
||||
fun openGraph(v: View) {
|
||||
analytics.trackClickEvent(
|
||||
ClickEvent("GraphClick")
|
||||
.addProperty("Instrument", ticker)
|
||||
)
|
||||
val intent = Intent(this, GraphActivity::class.java)
|
||||
intent.putExtra(GraphActivity.TICKER, ticker)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun addTickerToWidget(
|
||||
ticker: String,
|
||||
widgetId: Int
|
||||
) {
|
||||
val widgetData = widgetDataProvider.dataForWidgetId(widgetId)
|
||||
if (!widgetData.hasTicker(ticker)) {
|
||||
widgetData.addTicker(ticker)
|
||||
widgetDataProvider.broadcastUpdateWidget(widgetId)
|
||||
val addMenuItem = toolbar.menu.findItem(R.id.action_add)
|
||||
val removeMenuItem = toolbar.menu.findItem(R.id.action_remove)
|
||||
addMenuItem.isVisible = false
|
||||
removeMenuItem.isVisible = true
|
||||
InAppMessage.showMessage(this, getString(R.string.added_to_list, ticker))
|
||||
} else {
|
||||
showDialog(getString(R.string.already_in_portfolio, ticker))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import com.github.premnirmal.ticker.components.InAppMessage
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.events.RefreshEvent
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import com.github.premnirmal.ticker.news.NewsFeedActivity
|
||||
import com.github.premnirmal.ticker.news.QuoteDetailActivity
|
||||
import com.github.premnirmal.ticker.portfolio.StocksAdapter.QuoteClickListener
|
||||
import com.github.premnirmal.ticker.portfolio.drag_drop.OnStartDragListener
|
||||
import com.github.premnirmal.ticker.portfolio.drag_drop.SimpleItemTouchHelperCallback
|
||||
@@ -29,7 +29,6 @@ import kotlinx.android.synthetic.main.portfolio_fragment.stockList
|
||||
import kotlinx.android.synthetic.main.portfolio_fragment.view_flipper
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -96,8 +95,8 @@ class PortfolioFragment : BaseFragment(), QuoteClickListener, OnStartDragListene
|
||||
) {
|
||||
analytics.trackClickEvent(ClickEvent("InstrumentClick")
|
||||
.addProperty("Instrument", quote.symbol))
|
||||
val intent = Intent(view.context, NewsFeedActivity::class.java)
|
||||
intent.putExtra(NewsFeedActivity.TICKER, quote.symbol)
|
||||
val intent = Intent(view.context, QuoteDetailActivity::class.java)
|
||||
intent.putExtra(QuoteDetailActivity.TICKER, quote.symbol)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@@ -107,7 +106,7 @@ class PortfolioFragment : BaseFragment(), QuoteClickListener, OnStartDragListene
|
||||
position: Int
|
||||
) {
|
||||
val popupWindow = PopupMenu(view.context, view)
|
||||
popupWindow.menuInflater.inflate(R.menu.stock_menu, popupWindow.menu)
|
||||
popupWindow.menuInflater.inflate(R.menu.menu_portfolio, popupWindow.menu)
|
||||
val moveToWidgetItem = popupWindow.menu.findItem(R.id.move_to_widget)
|
||||
moveToWidgetItem.isEnabled = widgetId !=
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID && holder.widgetDataProvider.getAppWidgetIds().size >
|
||||
|
||||
+76
-23
@@ -2,6 +2,7 @@ package com.github.premnirmal.ticker.portfolio.search
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
@@ -19,7 +20,8 @@ import com.github.premnirmal.ticker.getStatusBarHeight
|
||||
import com.github.premnirmal.ticker.home.ChildFragment
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.ticker.network.StocksApi
|
||||
import com.github.premnirmal.ticker.network.data.Suggestions.Suggestion
|
||||
import com.github.premnirmal.ticker.network.data.Suggestion
|
||||
import com.github.premnirmal.ticker.news.QuoteDetailActivity
|
||||
import com.github.premnirmal.ticker.portfolio.search.SuggestionsAdapter.SuggestionClickListener
|
||||
import com.github.premnirmal.ticker.showDialog
|
||||
import com.github.premnirmal.ticker.widget.WidgetDataProvider
|
||||
@@ -27,7 +29,9 @@ import com.github.premnirmal.tickerwidget.R
|
||||
import kotlinx.android.synthetic.main.fragment_search.fake_status_bar
|
||||
import kotlinx.android.synthetic.main.fragment_search.recycler_view
|
||||
import kotlinx.android.synthetic.main.fragment_search.search_view
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -90,6 +94,19 @@ class SearchFragment : BaseFragment(), ChildFragment, SuggestionClickListener, T
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
adapter.getData()
|
||||
.forEach {
|
||||
it.exists = stocksProvider.hasTicker(it.symbol)
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTickerToWidget(
|
||||
ticker: String,
|
||||
widgetId: Int
|
||||
@@ -131,7 +148,14 @@ class SearchFragment : BaseFragment(), ChildFragment, SuggestionClickListener, T
|
||||
lifecycleScope.launch {
|
||||
val suggestions = stocksApi.getSuggestions(query)
|
||||
if (suggestions.wasSuccessful) {
|
||||
adapter.setData(suggestions.data)
|
||||
val data = withContext(Dispatchers.Default) {
|
||||
suggestions.data.map {
|
||||
val sug = Suggestion.fromSuggestionNet(it)
|
||||
sug.exists = stocksProvider.hasTicker(sug.symbol)
|
||||
sug
|
||||
}
|
||||
}
|
||||
adapter.setData(data)
|
||||
} else {
|
||||
Timber.w(suggestions.error)
|
||||
InAppMessage.showMessage(requireActivity(), R.string.error_fetching_suggestions, error = true)
|
||||
@@ -143,34 +167,63 @@ class SearchFragment : BaseFragment(), ChildFragment, SuggestionClickListener, T
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuggestionClick(suggestion: Suggestion) {
|
||||
override fun onSuggestionClick(suggestion: Suggestion): Boolean {
|
||||
val ticker = suggestion.symbol
|
||||
if (selectedWidgetId > 0) {
|
||||
addTickerToWidget(ticker, selectedWidgetId)
|
||||
return
|
||||
return true
|
||||
}
|
||||
if (widgetDataProvider.hasWidget()) {
|
||||
val widgetIds = widgetDataProvider.getAppWidgetIds()
|
||||
if (widgetIds.size > 1) {
|
||||
val widgets =
|
||||
widgetIds.map { widgetDataProvider.dataForWidgetId(it) }
|
||||
.sortedBy { it.widgetName() }
|
||||
val widgetNames = widgets.map { it.widgetName() }
|
||||
.toTypedArray()
|
||||
AlertDialog.Builder(context!!)
|
||||
.setTitle(R.string.select_widget)
|
||||
.setItems(widgetNames) { dialog, which ->
|
||||
val id = widgets[which].widgetId
|
||||
addTickerToWidget(ticker, id)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
val intent = Intent(requireContext(), QuoteDetailActivity::class.java)
|
||||
intent.putExtra(QuoteDetailActivity.TICKER, ticker)
|
||||
startActivity(intent)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onAddRemoveClick(suggestion: Suggestion): Boolean {
|
||||
val ticker = suggestion.symbol
|
||||
if (!suggestion.exists) {
|
||||
if (selectedWidgetId > 0) {
|
||||
addTickerToWidget(ticker, selectedWidgetId)
|
||||
return true
|
||||
} else {
|
||||
addTickerToWidget(ticker, widgetIds.first())
|
||||
if (widgetDataProvider.hasWidget()) {
|
||||
val widgetIds = widgetDataProvider.getAppWidgetIds()
|
||||
if (widgetIds.size > 1) {
|
||||
val widgets =
|
||||
widgetIds.map { widgetDataProvider.dataForWidgetId(it) }
|
||||
.sortedBy { it.widgetName() }
|
||||
val widgetNames = widgets.map { it.widgetName() }
|
||||
.toTypedArray()
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.select_widget)
|
||||
.setItems(widgetNames) { dialog, which ->
|
||||
val id = widgets[which].widgetId
|
||||
addTickerToWidget(ticker, id)
|
||||
suggestion.exists = !suggestion.exists
|
||||
adapter.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
return false
|
||||
} else {
|
||||
addTickerToWidget(ticker, widgetIds.first())
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
addTickerToWidget(ticker, WidgetDataProvider.INVALID_WIDGET_ID)
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addTickerToWidget(ticker, WidgetDataProvider.INVALID_WIDGET_ID)
|
||||
if (selectedWidgetId > 0) {
|
||||
val widgetData = widgetDataProvider.dataForWidgetId(selectedWidgetId)
|
||||
widgetData.removeStock(ticker)
|
||||
} else {
|
||||
val widgetData = widgetDataProvider.widgetDataWithStock(ticker)
|
||||
widgetData?.removeStock(ticker)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-8
@@ -4,8 +4,10 @@ import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.github.premnirmal.ticker.network.data.Suggestions.Suggestion
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.premnirmal.ticker.network.data.Suggestion
|
||||
import com.github.premnirmal.ticker.portfolio.search.SuggestionsAdapter.SuggestionVH
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
import java.util.ArrayList
|
||||
@@ -14,12 +16,13 @@ import java.util.ArrayList
|
||||
* Created by premnirmal on 2/26/16.
|
||||
*/
|
||||
internal class SuggestionsAdapter(private val suggestionClickListener: SuggestionClickListener) :
|
||||
androidx.recyclerview.widget.RecyclerView.Adapter<SuggestionVH>() {
|
||||
RecyclerView.Adapter<SuggestionVH>() {
|
||||
|
||||
private val suggestions: MutableList<Suggestion> = ArrayList()
|
||||
|
||||
interface SuggestionClickListener {
|
||||
fun onSuggestionClick(suggestion: Suggestion)
|
||||
fun onSuggestionClick(suggestion: Suggestion): Boolean
|
||||
fun onAddRemoveClick(suggestion: Suggestion): Boolean
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
@@ -36,6 +39,8 @@ internal class SuggestionsAdapter(private val suggestionClickListener: Suggestio
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun getData() = suggestions as List<Suggestion>
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
@@ -61,15 +66,28 @@ internal class SuggestionsAdapter(private val suggestionClickListener: Suggestio
|
||||
class SuggestionVH(
|
||||
itemView: View,
|
||||
private val suggestionClickListener: SuggestionClickListener
|
||||
) :
|
||||
androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView) {
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
private val textView: TextView = itemView as TextView
|
||||
private val textView: TextView = itemView.findViewById(R.id.suggestion_text)
|
||||
private val addRemoveImage: ImageView = itemView.findViewById(R.id.add_remove_image)
|
||||
private var suggestion: Suggestion? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener { _ ->
|
||||
suggestion?.let { suggestionClickListener.onSuggestionClick(it) }
|
||||
textView.setOnClickListener { _ ->
|
||||
suggestion?.let {
|
||||
if (suggestionClickListener.onSuggestionClick(it)) {
|
||||
it.exists = !it.exists
|
||||
update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
addRemoveImage.setOnClickListener {
|
||||
suggestion?.let {
|
||||
if(suggestionClickListener.onAddRemoveClick(it)) {
|
||||
it.exists = !it.exists
|
||||
update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +105,9 @@ internal class SuggestionsAdapter(private val suggestionClickListener: Suggestio
|
||||
}
|
||||
val name = builder.toString()
|
||||
textView.text = name
|
||||
addRemoveImage.setImageResource(
|
||||
if (item.exists) R.drawable.ic_remove_circle else R.drawable.ic_add_circle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ class SettingsFragment : PreferenceFragmentCompat(), ChildFragment,
|
||||
savedInstanceState: Bundle?
|
||||
) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(toolbar.layoutParams as MarginLayoutParams).topMargin = context!!.getStatusBarHeight()
|
||||
(toolbar.layoutParams as MarginLayoutParams).topMargin = requireContext().getStatusBarHeight()
|
||||
listView.addItemDecoration(DividerItemDecoration(activity, DividerItemDecoration.VERTICAL))
|
||||
listView.isVerticalScrollBarEnabled = false
|
||||
setupSimplePreferencesScreen()
|
||||
@@ -175,9 +175,9 @@ class SettingsFragment : PreferenceFragmentCompat(), ChildFragment,
|
||||
.setShowTitle(true)
|
||||
.setCloseButtonIcon(resources.getDrawable(R.drawable.ic_close).toBitmap())
|
||||
.build()
|
||||
CustomTabsHelper.addKeepAliveExtra(context!!, customTabsIntent.intent)
|
||||
CustomTabsHelper.addKeepAliveExtra(requireContext(), customTabsIntent.intent)
|
||||
CustomTabsHelper.openCustomTab(
|
||||
context!!, customTabsIntent,
|
||||
requireContext(), customTabsIntent,
|
||||
Uri.parse(resources.getString(R.string.privacy_policy_url)),
|
||||
BrowserFallback()
|
||||
)
|
||||
@@ -207,7 +207,7 @@ class SettingsFragment : PreferenceFragmentCompat(), ChildFragment,
|
||||
preferences.edit()
|
||||
.clear()
|
||||
.apply()
|
||||
val filesDir = context!!.filesDir
|
||||
val filesDir = requireContext().filesDir
|
||||
filesDir.listFiles()
|
||||
.forEach { file ->
|
||||
file?.delete()
|
||||
@@ -482,7 +482,7 @@ class SettingsFragment : PreferenceFragmentCompat(), ChildFragment,
|
||||
return
|
||||
}
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
FileProvider.getUriForFile(context!!, BuildConfig.APPLICATION_ID + ".provider", file)
|
||||
FileProvider.getUriForFile(requireContext(), BuildConfig.APPLICATION_ID + ".provider", file)
|
||||
} else {
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class WidgetSettingsActivity : BaseActivity(), WidgetSettingsFragment.Parent {
|
||||
}
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.fragment_container, WidgetSettingsFragment.newInstance(widgetId))
|
||||
.add(R.id.fragment_container, WidgetSettingsFragment.newInstance(widgetId, true))
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -10,8 +10,10 @@ import android.view.View.OnClickListener
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.ArrayRes
|
||||
import com.github.premnirmal.ticker.base.BaseFragment
|
||||
import com.github.premnirmal.ticker.components.AsyncBus
|
||||
import com.github.premnirmal.ticker.components.InAppMessage
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.events.RefreshEvent
|
||||
import com.github.premnirmal.ticker.showDialog
|
||||
import com.github.premnirmal.ticker.ui.SettingsTextView
|
||||
import com.github.premnirmal.ticker.widget.WidgetData
|
||||
@@ -33,12 +35,14 @@ import javax.inject.Inject
|
||||
class WidgetSettingsFragment : BaseFragment(), OnClickListener {
|
||||
|
||||
companion object {
|
||||
const val ARG_WIDGET_ID = AppWidgetManager.EXTRA_APPWIDGET_ID
|
||||
private const val ARG_WIDGET_ID = AppWidgetManager.EXTRA_APPWIDGET_ID
|
||||
private const val ARG_SHOW_ADD_STOCKS = "show_add_stocks"
|
||||
|
||||
fun newInstance(widgetId: Int): WidgetSettingsFragment {
|
||||
fun newInstance(widgetId: Int, showAddStocks: Boolean = true): WidgetSettingsFragment {
|
||||
val fragment = WidgetSettingsFragment()
|
||||
val args = Bundle()
|
||||
args.putInt(ARG_WIDGET_ID, widgetId)
|
||||
args.putBoolean(ARG_SHOW_ADD_STOCKS, showAddStocks)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
@@ -49,6 +53,8 @@ class WidgetSettingsFragment : BaseFragment(), OnClickListener {
|
||||
}
|
||||
|
||||
@Inject internal lateinit var widgetDataProvider: WidgetDataProvider
|
||||
@Inject internal lateinit var bus: AsyncBus
|
||||
private var showAddStocks = true
|
||||
private val parent: Parent
|
||||
get() = activity as Parent
|
||||
internal var widgetId = 0
|
||||
@@ -58,6 +64,7 @@ class WidgetSettingsFragment : BaseFragment(), OnClickListener {
|
||||
super.onCreate(savedInstanceState)
|
||||
Injector.appComponent.inject(this)
|
||||
widgetId = arguments!!.getInt(ARG_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
showAddStocks = arguments!!.getBoolean(ARG_SHOW_ADD_STOCKS, true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -73,6 +80,7 @@ class WidgetSettingsFragment : BaseFragment(), OnClickListener {
|
||||
savedInstanceState: Bundle?
|
||||
) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setting_add_stock.visibility = if (showAddStocks) View.VISIBLE else View.GONE
|
||||
val widgetData = widgetDataProvider.dataForWidgetId(widgetId)
|
||||
setWidgetNameSetting(widgetData)
|
||||
setLayoutTypeSetting(widgetData)
|
||||
@@ -103,6 +111,7 @@ class WidgetSettingsFragment : BaseFragment(), OnClickListener {
|
||||
setWidgetNameSetting(widgetData)
|
||||
v.setIsEditable(false)
|
||||
v.setOnClickListener(this)
|
||||
bus.send(RefreshEvent())
|
||||
InAppMessage.showMessage(activity!!, R.string.widget_name_updated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Bundle
|
||||
@@ -18,6 +17,7 @@ import com.github.premnirmal.ticker.home.ParanormalActivity
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
import com.github.premnirmal.tickerwidget.R.layout
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -97,9 +97,14 @@ class StockWidget : AppWidgetProvider() {
|
||||
appWidgetIds: IntArray?
|
||||
) {
|
||||
super.onDeleted(context, appWidgetIds)
|
||||
appWidgetIds?.let {
|
||||
it.forEach { widgetId ->
|
||||
widgetDataProvider.widgetRemoved(widgetId)
|
||||
appWidgetIds?.let { id ->
|
||||
id.forEach { widgetId ->
|
||||
val removed = widgetDataProvider.removeWidget(widgetId)
|
||||
removed?.getTickers()?.forEach { ticker ->
|
||||
if (!widgetDataProvider.containsTicker(ticker)) {
|
||||
stocksProvider.removeStock(ticker)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -56,10 +57,11 @@ class WidgetDataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fun widgetRemoved(widgetId: Int) {
|
||||
synchronized(widgets) {
|
||||
fun removeWidget(widgetId: Int): WidgetData? {
|
||||
return synchronized(widgets) {
|
||||
val removed = widgets.remove(widgetId)
|
||||
removed?.onWidgetRemoved()
|
||||
return@synchronized removed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +85,9 @@ class WidgetDataProvider {
|
||||
|
||||
fun containsTicker(ticker: String): Boolean = widgets.any { it.value.hasTicker(ticker) }
|
||||
|
||||
fun widgetDataWithStock(ticker: String) =
|
||||
widgets.filter { it.value.hasTicker(ticker) }.values.firstOrNull()
|
||||
|
||||
fun moveQuoteToDifferentWidget(
|
||||
oldWidgetId: Int,
|
||||
quote: Quote,
|
||||
|
||||
@@ -81,7 +81,7 @@ class WidgetsFragment : BaseFragment(), ChildFragment, OnItemSelectedListener {
|
||||
}
|
||||
|
||||
private fun setWidgetFragment(widgetId: Int) {
|
||||
val fragment = WidgetSettingsFragment.newInstance(widgetId)
|
||||
val fragment = WidgetSettingsFragment.newInstance(widgetId, false)
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.child_fragment_container, fragment)
|
||||
.commit()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M13,7h-2v4L7,11v2h4v4h2v-4h4v-2h-4L13,7zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,13H5v-2h14v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M7,11v2h10v-2L7,11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
|
||||
</vector>
|
||||
@@ -17,11 +17,11 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@color/card_bg"
|
||||
android:background="@color/bottom_bar_bg"
|
||||
app:elevation="4dp"
|
||||
app:itemIconTint="@drawable/bottom_nav_icon_tint"
|
||||
app:itemTextColor="@drawable/bottom_nav_icon_tint"
|
||||
app:menu="@menu/bottom_nav_items"
|
||||
app:menu="@menu/menu_bottom_nav"
|
||||
/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
+2
@@ -125,6 +125,7 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/positions_header"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
@@ -154,6 +155,7 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/positions_container"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/card_bg"
|
||||
android:orientation="vertical"
|
||||
@@ -1,19 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/suggestion_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:lines="1"
|
||||
android:maxLines="1"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="@dimen/text_size"
|
||||
tools:text="GOOG - Alphabet Inc (NASDAQ)"
|
||||
/>
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
>
|
||||
<TextView
|
||||
android:id="@+id/suggestion_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:lines="1"
|
||||
android:maxLines="1"
|
||||
android:textSize="@dimen/text_size"
|
||||
tools:text="GOOG - Alphabet Inc (NASDAQ)"
|
||||
/>
|
||||
<ImageView
|
||||
android:id="@+id/add_remove_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:layout_gravity="center"
|
||||
tools:src="@drawable/ic_add_circle"
|
||||
/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_add"
|
||||
android:icon="@drawable/ic_add_circle"
|
||||
android:title="@string/add"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_remove"
|
||||
android:icon="@drawable/ic_remove_circle"
|
||||
android:title="@string/remove"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<!-- v21 specific -->
|
||||
<item name="android:statusBarColor">@color/transparent</item>
|
||||
<item name="android:navigationBarColor">@color/default_dark_bg</item>
|
||||
<item name="android:navigationBarColor">@color/nav_bar_bg</item>
|
||||
</style>
|
||||
|
||||
<!-- Custom Preference Theme -->
|
||||
@@ -35,6 +35,10 @@
|
||||
<item name="layout">@layout/fragment_settings</item>
|
||||
</style>
|
||||
|
||||
<style name="ParanormalActivityTheme" parent="AppTheme">
|
||||
<item name="android:navigationBarColor">@color/nav_bar_bg_2</item>
|
||||
</style>
|
||||
|
||||
<style name="WidgetSettingsTheme" parent="AppTheme">
|
||||
<item name="android:statusBarColor">@color/color_primary</item>
|
||||
</style>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<!-- v21 specific -->
|
||||
<item name="android:statusBarColor">@color/transparent</item>
|
||||
<item name="android:navigationBarColor">@color/default_dark_bg</item>
|
||||
<item name="android:navigationBarColor">@color/nav_bar_bg</item>
|
||||
</style>
|
||||
<!-- Custom Preference Theme -->
|
||||
<style name="AppPreferenceTheme" parent="@style/PreferenceThemeOverlay">
|
||||
@@ -37,6 +37,10 @@
|
||||
<item name="layout">@layout/fragment_settings</item>
|
||||
</style>
|
||||
|
||||
<style name="ParanormalActivityTheme" parent="AppTheme">
|
||||
<item name="android:navigationBarColor">@color/nav_bar_bg_2</item>
|
||||
</style>
|
||||
|
||||
<style name="WidgetSettingsTheme" parent="AppTheme">
|
||||
<item name="android:statusBarColor">@color/color_primary</item>
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="colorPrimary">@color/color_primary</item>
|
||||
<item name="colorPrimaryDark">@color/color_primary_dark</item>
|
||||
<item name="colorAccent">@color/color_accent</item>
|
||||
<item name="android:colorPrimary">@color/color_primary</item>
|
||||
<item name="android:colorPrimaryDark">@color/color_primary_dark</item>
|
||||
<item name="android:windowBackground">@color/default_dark_bg</item>
|
||||
<item name="android:colorAccent">@color/color_accent</item>
|
||||
<item name="android:colorSecondary">@color/color_accent</item>
|
||||
<item name="android:alertDialogTheme">@style/AlertDialog</item>
|
||||
<item name="android:dialogTheme">@style/AlertDialog</item>
|
||||
<item name="android:actionBarStyle">@style/ActionBarStyle</item>
|
||||
|
||||
<item name="android:textViewStyle">@style/TextViewStyle</item>
|
||||
<item name="android:buttonStyle">@style/ButtonStyle</item>
|
||||
|
||||
<!-- For AppCompat -->
|
||||
<item name="editTextStyle">@style/EditTextStyle</item>
|
||||
<item name="buttonStyle">@style/ButtonStyle</item>
|
||||
<item name="preferenceTheme">@style/AppPreferenceTheme</item>
|
||||
|
||||
<!-- v21 specific -->
|
||||
<item name="android:statusBarColor">@color/transparent</item>
|
||||
<item name="android:navigationBarColor">@color/nav_bar_bg</item>
|
||||
<!-- v27 specific -->
|
||||
<item name="android:navigationBarDividerColor">@color/transparent</item>
|
||||
</style>
|
||||
<!-- Custom Preference Theme -->
|
||||
<style name="AppPreferenceTheme" parent="@style/PreferenceThemeOverlay">
|
||||
<item name="preferenceFragmentCompatStyle">@style/AppPreferenceFragmentCompatStyle</item>
|
||||
<item name="preferenceFragmentStyle">@style/AppPreferenceFragmentCompatStyle</item>
|
||||
</style>
|
||||
<!-- Custom Style for PreferenceFragmentCompat -->
|
||||
<style name="AppPreferenceFragmentCompatStyle" parent="@style/PreferenceFragment.Material">
|
||||
<item name="android:layout">@layout/fragment_settings</item>
|
||||
<item name="layout">@layout/fragment_settings</item>
|
||||
</style>
|
||||
|
||||
<style name="ParanormalActivityTheme" parent="AppTheme">
|
||||
<item name="android:navigationBarColor">@color/nav_bar_bg_2</item>
|
||||
</style>
|
||||
|
||||
<style name="WidgetSettingsTheme" parent="AppTheme">
|
||||
<item name="android:statusBarColor">@color/color_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="SearchTheme" parent="AppTheme">
|
||||
<item name="android:statusBarColor">@color/color_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="NewsFeedActivityTheme" parent="AppTheme">
|
||||
<item name="android:statusBarColor">@color/color_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="GraphActivityTheme" parent="AppTheme">
|
||||
<item name="android:navigationBarColor">@color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="AddPositionTheme" parent="AppTheme">
|
||||
<item name="android:statusBarColor">@color/color_primary</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -18,6 +18,9 @@
|
||||
<color name="translucent">#98000000</color>
|
||||
<color name="bg_gradient_center">#752a2a2a</color>
|
||||
<color name="black_gradient_center">#3f3f3f</color>
|
||||
<color name="nav_bar_bg">@color/default_dark_bg</color>
|
||||
<color name="nav_bar_bg_2">@color/card_bg</color>
|
||||
<color name="bottom_bar_bg">@color/card_bg</color>
|
||||
|
||||
<color name="white">#ffffff</color>
|
||||
<color name="black">#000000</color>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
versionName=3.4.3
|
||||
versionCode=343
|
||||
versionName=3.4.8
|
||||
versionCode=348
|
||||
Reference in New Issue
Block a user