WIP: account stats implementation
This commit is contained in:
parent
42361c9fbe
commit
8094bbec4e
|
@ -81,8 +81,10 @@ subprojects {
|
|||
StethoBeanShellREPL : '0.5',
|
||||
ArchLifecycleExtensions: '1.1.1',
|
||||
ArchPaging : '1.0.0',
|
||||
Room : '1.1.0',
|
||||
ConstraintLayout : '1.1.0',
|
||||
MessageBubbleView : '2.1',
|
||||
WorkManager : '1.0.0-alpha01',
|
||||
]
|
||||
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
org.gradle.jvmargs=-Xmx3584m
|
||||
org.gradle.jvmargs=-Xmx3584m
|
||||
android.databinding.enableV2=true
|
|
@ -38,6 +38,7 @@ import java.lang.annotation.RetentionPolicy;
|
|||
CustomTabType.LIST_TIMELINE,
|
||||
CustomTabType.PUBLIC_TIMELINE,
|
||||
CustomTabType.NETWORK_PUBLIC_TIMELINE,
|
||||
CustomTabType.ACCOUNT_STATS,
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface CustomTabType {
|
||||
|
@ -52,4 +53,5 @@ public @interface CustomTabType {
|
|||
String LIST_TIMELINE = "list_timeline";
|
||||
String PUBLIC_TIMELINE = "public_timeline";
|
||||
String NETWORK_PUBLIC_TIMELINE = "network_public_timeline";
|
||||
String ACCOUNT_STATS = "account_stats";
|
||||
}
|
||||
|
|
|
@ -104,7 +104,8 @@ public class TabArguments implements Parcelable {
|
|||
case CustomTabType.DIRECT_MESSAGES:
|
||||
case CustomTabType.TRENDS_SUGGESTIONS:
|
||||
case CustomTabType.PUBLIC_TIMELINE:
|
||||
case CustomTabType.NETWORK_PUBLIC_TIMELINE: {
|
||||
case CustomTabType.NETWORK_PUBLIC_TIMELINE:
|
||||
case CustomTabType.ACCOUNT_STATS: {
|
||||
return LoganSquare.parse(json, TabArguments.class);
|
||||
}
|
||||
case CustomTabType.USER_TIMELINE:
|
||||
|
|
|
@ -57,6 +57,16 @@ android {
|
|||
additionalParameters '--no-version-vectors'
|
||||
}
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
flavorDimensions 'channel'
|
||||
|
||||
productFlavors {
|
||||
|
@ -164,6 +174,7 @@ dependencies {
|
|||
kapt "com.google.dagger:dagger-compiler:${libVersions['Dagger']}"
|
||||
kapt "com.github.mariotaku.ObjectCursor:processor:${libVersions['ObjectCursor']}"
|
||||
kapt "com.github.bumptech.glide:compiler:${libVersions['Glide']}"
|
||||
kapt "android.arch.persistence.room:compiler:${libVersions['Room']}"
|
||||
|
||||
implementation project(':twidere.component.common')
|
||||
implementation project(':twidere.component.nyan')
|
||||
|
@ -204,6 +215,8 @@ dependencies {
|
|||
|
||||
implementation "android.arch.lifecycle:extensions:${libVersions['ArchLifecycleExtensions']}"
|
||||
implementation "android.arch.paging:runtime:${libVersions['ArchPaging']}"
|
||||
implementation "android.arch.persistence.room:runtime:${libVersions['Room']}"
|
||||
implementation "android.arch.work:work-runtime-ktx:${libVersions['WorkManager']}"
|
||||
implementation "com.android.support:multidex:${libVersions['MultiDex']}"
|
||||
implementation "com.android.support:support-annotations:${libVersions['SupportLib']}"
|
||||
implementation "com.android.support:support-compat:${libVersions['SupportLib']}"
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "df1fe758472ff9d1d8e21da6f4d8b5fc",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "account_daily_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `createdAt` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `statusesCount` INTEGER NOT NULL, `followersCount` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountKey",
|
||||
"columnName": "accountKey",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusesCount",
|
||||
"columnName": "statusesCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "followersCount",
|
||||
"columnName": "followersCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"df1fe758472ff9d1d8e21da6f4d8b5fc\")"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "9ab8ea60785ca135c5f1dc50547c143f",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "account_daily_stats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `createdAt` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `statusesCount` INTEGER NOT NULL, `followersCount` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountKey",
|
||||
"columnName": "accountKey",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusesCount",
|
||||
"columnName": "statusesCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "followersCount",
|
||||
"columnName": "followersCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_account_daily_stats_createdAt_accountKey",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"createdAt",
|
||||
"accountKey"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX `index_account_daily_stats_createdAt_accountKey` ON `${TABLE_NAME}` (`createdAt`, `accountKey`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9ab8ea60785ca135c5f1dc50547c143f\")"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -126,7 +126,8 @@ public class CustomTabUtils implements Constants {
|
|||
case CustomTabType.DIRECT_MESSAGES:
|
||||
case CustomTabType.TRENDS_SUGGESTIONS:
|
||||
case CustomTabType.PUBLIC_TIMELINE:
|
||||
case CustomTabType.NETWORK_PUBLIC_TIMELINE: {
|
||||
case CustomTabType.NETWORK_PUBLIC_TIMELINE:
|
||||
case CustomTabType.ACCOUNT_STATS: {
|
||||
return new TabArguments();
|
||||
}
|
||||
case CustomTabType.USER_TIMELINE:
|
||||
|
|
|
@ -62,6 +62,8 @@ import android.view.View.OnClickListener
|
|||
import android.view.View.OnLongClickListener
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.Toast
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.ktx.OneTimeWorkRequestBuilder
|
||||
import com.getkeepsafe.taptargetview.TapTarget
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.squareup.otto.Subscribe
|
||||
|
@ -103,6 +105,7 @@ import org.mariotaku.twidere.receiver.NotificationReceiver
|
|||
import org.mariotaku.twidere.service.StreamingService
|
||||
import org.mariotaku.twidere.singleton.BusSingleton
|
||||
import org.mariotaku.twidere.singleton.PreferencesSingleton
|
||||
import org.mariotaku.twidere.task.worker.AccountDailyStatWorker
|
||||
import org.mariotaku.twidere.util.*
|
||||
import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback
|
||||
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
|
||||
|
@ -293,6 +296,8 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
|
|||
if (!showDrawerTutorial() && !PreferencesSingleton.get(this)[defaultAutoRefreshAskedKey]) {
|
||||
showAutoRefreshConfirm()
|
||||
}
|
||||
|
||||
WorkManager.getInstance().enqueue(OneTimeWorkRequestBuilder<AccountDailyStatWorker>().build())
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package org.mariotaku.twidere.content.database
|
||||
|
||||
import android.arch.persistence.room.Database
|
||||
import android.arch.persistence.room.Room
|
||||
import android.arch.persistence.room.RoomDatabase
|
||||
import android.arch.persistence.room.TypeConverters
|
||||
import org.mariotaku.twidere.content.database.converter.UserKeyConverter
|
||||
import org.mariotaku.twidere.content.database.dao.AccountDailyStats
|
||||
import org.mariotaku.twidere.content.model.AccountStats
|
||||
import org.mariotaku.twidere.util.lang.ApplicationContextSingletonHolder
|
||||
|
||||
@Database(entities = [AccountStats::class], version = 2)
|
||||
@TypeConverters(UserKeyConverter::class)
|
||||
abstract class TwidereDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun accountDailyStats(): AccountDailyStats
|
||||
|
||||
companion object : ApplicationContextSingletonHolder<TwidereDatabase>({
|
||||
Room.databaseBuilder(it, TwidereDatabase::class.java, "twidere.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
})
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package org.mariotaku.twidere.content.database.converter
|
||||
|
||||
import android.arch.persistence.room.TypeConverter
|
||||
import org.mariotaku.twidere.extension.lang.threadLocal
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class LocalDateConverter {
|
||||
|
||||
@TypeConverter
|
||||
fun parse(time: String): Date {
|
||||
return simpleDateFormat.parse(time)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun serialize(date: Date): String {
|
||||
return simpleDateFormat.format(date)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val simpleDateFormat by threadLocal {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.mariotaku.twidere.content.database.converter
|
||||
|
||||
import android.arch.persistence.room.TypeConverter
|
||||
import java.util.*
|
||||
|
||||
class TimestampDateTimeConverter {
|
||||
|
||||
@TypeConverter
|
||||
fun parse(time: Long): Date {
|
||||
return Date(time)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun serialize(date: Date): Long {
|
||||
return date.time
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package org.mariotaku.twidere.content.database.converter
|
||||
|
||||
import android.arch.persistence.room.TypeConverter
|
||||
import org.mariotaku.twidere.model.UserKey
|
||||
|
||||
class UserKeyConverter {
|
||||
|
||||
@TypeConverter
|
||||
fun parse(str: String): UserKey {
|
||||
return UserKey.valueOf(str)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun serialize(key: UserKey): String {
|
||||
return key.toString()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
package org.mariotaku.twidere.content.database.dao
|
||||
|
||||
import android.arch.persistence.room.*
|
||||
import org.mariotaku.twidere.content.database.converter.LocalDateConverter
|
||||
import org.mariotaku.twidere.content.model.AccountStats
|
||||
import org.mariotaku.twidere.extension.julianDay
|
||||
import org.mariotaku.twidere.extension.time
|
||||
import org.mariotaku.twidere.model.UserKey
|
||||
import java.util.*
|
||||
import kotlin.math.sign
|
||||
|
||||
@Dao
|
||||
abstract class AccountDailyStats {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insert(stats: AccountStats)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insert(stats: Collection<AccountStats>)
|
||||
|
||||
@Query("SELECT * FROM `account_daily_stats` WHERE `createdAt` = :date")
|
||||
abstract fun list(@TypeConverters(LocalDateConverter::class) date: Date): List<AccountStats>
|
||||
|
||||
@Query("SELECT * FROM `account_daily_stats` WHERE `accountKey` = :accountKey AND `createdAt` BETWEEN :since AND :until ORDER BY `createdAt`")
|
||||
abstract fun list(accountKey: UserKey, @TypeConverters(LocalDateConverter::class) since: Date,
|
||||
@TypeConverters(LocalDateConverter::class) until: Date): List<AccountStats>
|
||||
|
||||
@Query("SELECT * FROM `account_daily_stats` WHERE `accountKey` = :accountKey AND `createdAt` >= :since ORDER BY `createdAt` LIMIT 1")
|
||||
abstract fun firstSince(accountKey: UserKey, @TypeConverters(LocalDateConverter::class) since: Date): AccountStats?
|
||||
|
||||
fun listSparse(accountKey: UserKey, @TypeConverters(LocalDateConverter::class) since: Date,
|
||||
@TypeConverters(LocalDateConverter::class) until: Date): Array<AccountStats?> {
|
||||
val sinceJulianDay = Calendar.getInstance().time(since).julianDay()
|
||||
val count = Calendar.getInstance().time(until).julianDay() - sinceJulianDay + 1
|
||||
val result = arrayOfNulls<AccountStats>(count)
|
||||
val tempCal = Calendar.getInstance()
|
||||
list(accountKey, since, until).forEach {
|
||||
tempCal.time = it.createdAt
|
||||
val index = tempCal.julianDay() - sinceJulianDay
|
||||
if (index in result.indices) {
|
||||
result[index] = it
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun monthlySummary(accountKey: UserKey, date: Date): AccountStats.Summaries {
|
||||
val since = Calendar.getInstance().apply {
|
||||
time = date
|
||||
add(Calendar.DATE, -27)
|
||||
}.time
|
||||
|
||||
val prevPeriodFirst = firstSince(accountKey, Calendar.getInstance().apply {
|
||||
time = date
|
||||
add(Calendar.DATE, -55)
|
||||
}.time)
|
||||
|
||||
val currentPeriodStats = listSparse(accountKey, since, date)
|
||||
|
||||
val statuses = currentPeriodStats.statusesSummary(prevPeriodFirst)
|
||||
|
||||
val followers = currentPeriodStats.followersSummary()
|
||||
|
||||
return AccountStats.Summaries(statuses, followers)
|
||||
}
|
||||
|
||||
private fun Array<AccountStats?>.statusesSummary(prevPeriodFirst: AccountStats?): AccountStats.DisplaySummary {
|
||||
val firstNumber = numberAt(0, AccountStats::statusesCount)
|
||||
val lastNumber = numberAt(lastIndex, AccountStats::statusesCount)
|
||||
|
||||
var growthText: String? = null
|
||||
var growthSign = 0
|
||||
if (prevPeriodFirst != null) {
|
||||
val prevPeriodCount = firstNumber - prevPeriodFirst.statusesCount
|
||||
val currPeriodCount = lastNumber - firstNumber
|
||||
|
||||
val growthPercent = currPeriodCount / prevPeriodCount.toFloat() - 1
|
||||
growthText = String.format(Locale.US, "%.1f%%", Math.abs(growthPercent * 100))
|
||||
growthSign = growthPercent.sign.toInt()
|
||||
}
|
||||
|
||||
val firstNonNullIndex = indexOfFirst { it != null }
|
||||
if (firstNonNullIndex < 0) throw DataNotReadyException()
|
||||
|
||||
val diffs = LongArray(size - firstNonNullIndex) item@{ index ->
|
||||
val statIndex = index + firstNonNullIndex
|
||||
if (statIndex == 0) return@item 0
|
||||
return@item numberAt(statIndex, AccountStats::statusesCount) - numberAt(statIndex - 1,
|
||||
AccountStats::statusesCount)
|
||||
}
|
||||
var maxDiff = diffs.max()!!
|
||||
if (maxDiff <= 0L) {
|
||||
maxDiff = Math.abs(diffs.min()!!)
|
||||
}
|
||||
if (maxDiff <= 0L) {
|
||||
maxDiff = 10
|
||||
}
|
||||
|
||||
val values = FloatArray(size - firstNonNullIndex) item@{ index ->
|
||||
return@item diffs[index] / maxDiff.toFloat()
|
||||
}
|
||||
|
||||
val periodSum = lastNumber - firstNumber
|
||||
return AccountStats.DisplaySummary(periodSum, growthSign, growthText, size, values)
|
||||
}
|
||||
|
||||
private fun Array<AccountStats?>.followersSummary(): AccountStats.DisplaySummary {
|
||||
|
||||
val firstNumber = numberAt(0, AccountStats::followersCount)
|
||||
val lastNumber = numberAt(lastIndex, AccountStats::followersCount)
|
||||
|
||||
val maxNumber = maxBy { it?.followersCount ?: Long.MIN_VALUE }!!.followersCount
|
||||
val valuesCount = size
|
||||
val values = indices.map { index ->
|
||||
val number = numberAt(index, AccountStats::followersCount)
|
||||
return@map (number - firstNumber) / (maxNumber - firstNumber).toFloat()
|
||||
}.toFloatArray()
|
||||
|
||||
val growth = lastNumber - firstNumber
|
||||
return AccountStats.DisplaySummary(firstNumber, growth.sign, Math.abs(growth).toString(),
|
||||
valuesCount, values)
|
||||
}
|
||||
|
||||
private fun Array<AccountStats?>.numberAt(index: Int, selector: (AccountStats) -> Long): Long {
|
||||
val itemAt = this[index]
|
||||
if (itemAt != null) return selector(itemAt)
|
||||
val firstNonNullIndex = indexOfFirst { it != null }
|
||||
val lastNonNullIndex = indexOfLast { it != null }
|
||||
|
||||
if (firstNonNullIndex < 0 || lastNonNullIndex < 0 || firstNonNullIndex == lastNonNullIndex) {
|
||||
throw DataNotReadyException()
|
||||
}
|
||||
|
||||
val count = lastNonNullIndex - firstNonNullIndex
|
||||
val firstNonNullNumber = selector(this[firstNonNullIndex]!!)
|
||||
val lastNonNullNumber = selector(this[lastNonNullIndex]!!)
|
||||
val delta = (lastNonNullNumber - firstNonNullNumber) / count
|
||||
when {
|
||||
index < firstNonNullIndex -> return firstNonNullNumber - delta * (firstNonNullIndex - index)
|
||||
index > lastNonNullIndex -> return lastNonNullNumber + delta * (index - lastNonNullIndex)
|
||||
else -> {
|
||||
val startIndex = (0 until index).last { this[it] != null }
|
||||
val endIndex = (index + 1..lastIndex).first { this[it] != null }
|
||||
val start = selector(this[startIndex]!!)
|
||||
val end = selector(this[endIndex]!!)
|
||||
val delta2 = (end - start) / (endIndex - startIndex)
|
||||
return start + delta2 * (index - startIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataNotReadyException : Exception()
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package org.mariotaku.twidere.content.model
|
||||
|
||||
import android.arch.persistence.room.Entity
|
||||
import android.arch.persistence.room.Index
|
||||
import android.arch.persistence.room.PrimaryKey
|
||||
import android.arch.persistence.room.TypeConverters
|
||||
import org.mariotaku.twidere.content.database.converter.LocalDateConverter
|
||||
import org.mariotaku.twidere.model.UserKey
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "account_daily_stats",
|
||||
indices = [Index("createdAt", "accountKey", unique = true)])
|
||||
@TypeConverters(LocalDateConverter::class)
|
||||
data class AccountStats(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val rowId: Long = 0,
|
||||
val createdAt: Date,
|
||||
val accountKey: UserKey,
|
||||
val statusesCount: Long,
|
||||
val followersCount: Long
|
||||
) {
|
||||
|
||||
|
||||
data class Summaries(
|
||||
val statuses: DisplaySummary,
|
||||
val followers: DisplaySummary
|
||||
)
|
||||
|
||||
data class DisplaySummary(
|
||||
val number: Long,
|
||||
val growthSign: Int,
|
||||
val growth: String?,
|
||||
val valuesCount: Int,
|
||||
val values: FloatArray
|
||||
) {
|
||||
val numberDisplay: String
|
||||
get() = number.toString()
|
||||
|
||||
val growthDisplay: String?
|
||||
get() = when {
|
||||
growthSign < 0 -> "-$growth"
|
||||
growthSign > 0 -> "+$growth"
|
||||
else -> growth
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -24,6 +24,7 @@ import android.support.annotation.WorkerThread
|
|||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.mariotaku.twidere.util.DebugLog
|
||||
|
||||
abstract class ComputableLiveData<T>(loadOnInstantiate: Boolean) : LiveData<T>() {
|
||||
|
||||
|
@ -37,6 +38,7 @@ abstract class ComputableLiveData<T>(loadOnInstantiate: Boolean) : LiveData<T>()
|
|||
task(body = this::compute).successUi {
|
||||
postValue(it)
|
||||
}.failUi {
|
||||
DebugLog.e(msg = "Exception in ComputableLiveData", tr = it)
|
||||
postValue(null)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,12 +20,54 @@
|
|||
package org.mariotaku.twidere.extension
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private val epochCalendar = Calendar.getInstance().timeInMillis(0)
|
||||
val epochWeekDay: Int = epochCalendar.get(Calendar.DAY_OF_WEEK) - epochCalendar.getMinimum(Calendar.DAY_OF_WEEK)
|
||||
val epochJulianDay: Int = epochCalendar.julianDay()
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/9/25.
|
||||
*/
|
||||
fun Calendar.isSameDay(that: Calendar): Boolean {
|
||||
return this[Calendar.ERA] == that[Calendar.ERA] &&
|
||||
this[Calendar.YEAR] == that[Calendar.YEAR] &&
|
||||
this[Calendar.DAY_OF_YEAR] == that[Calendar.DAY_OF_YEAR]
|
||||
}
|
||||
}
|
||||
|
||||
fun Calendar.timeInMillis(time: Long): Calendar = apply { timeInMillis = time }
|
||||
fun Calendar.time(time: Date): Calendar = apply { this.time = time }
|
||||
|
||||
fun Calendar.daysSinceEpoch(day: Int): Calendar = apply {
|
||||
timeInMillis = 0
|
||||
add(Calendar.DATE, day)
|
||||
}
|
||||
|
||||
fun Calendar.clearClockTime() {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
clear(Calendar.MINUTE)
|
||||
clear(Calendar.SECOND)
|
||||
clear(Calendar.MILLISECOND)
|
||||
}
|
||||
|
||||
fun Calendar.dayProgress(): Float {
|
||||
val offsetHour = (get(Calendar.HOUR_OF_DAY) - 6 + 24) % 24
|
||||
val dayMillis = TimeUnit.HOURS.toMillis(offsetHour.toLong()) +
|
||||
TimeUnit.MINUTES.toMillis(get(Calendar.MINUTE).toLong()) +
|
||||
TimeUnit.SECONDS.toMillis(get(Calendar.SECOND).toLong()) +
|
||||
get(Calendar.MILLISECOND)
|
||||
return dayMillis / TimeUnit.HOURS.toMillis(24).toFloat()
|
||||
}
|
||||
|
||||
// http://en.wikipedia.org/wiki/Julian_day
|
||||
fun Calendar.julianDay(): Int {
|
||||
val year = get(Calendar.YEAR)
|
||||
val month = get(Calendar.MONTH) + 1
|
||||
val day = get(Calendar.DATE)
|
||||
|
||||
val a = (14 - month) / 12
|
||||
val y = year + 4800 - a
|
||||
val m = month + 12 * a - 3
|
||||
return day + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045
|
||||
}
|
||||
|
||||
fun Calendar.daysSinceEpoch(): Int = julianDay() - epochJulianDay
|
||||
|
||||
fun Calendar.weeksSinceEpoch(): Int = (daysSinceEpoch() + epochWeekDay) / 7
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Twidere - Twitter client for Android
|
||||
*
|
||||
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.mariotaku.twidere.extension.lang
|
||||
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class ThreadLocalDelegate<T>(initial: () -> T) {
|
||||
|
||||
private var threadLocal: ThreadLocal<T> = PropertyThreadLocal(initial)
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = threadLocal.get()
|
||||
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
threadLocal.set(value)
|
||||
}
|
||||
|
||||
private class PropertyThreadLocal<T>(val initial: () -> T) : ThreadLocal<T>() {
|
||||
|
||||
override fun initialValue(): T {
|
||||
return initial()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ThreadLocalProperty<T>(private val ref: ThreadLocal<T>) : ReadOnlyProperty<Any, T?> {
|
||||
|
||||
override fun getValue(thisRef: Any, property: KProperty<*>): T? = ref.get()
|
||||
}
|
||||
|
||||
fun <T> threadLocal(obj: () -> T): ThreadLocalDelegate<T> = ThreadLocalDelegate(obj)
|
||||
|
||||
operator fun <T> ThreadLocal<T>.provideDelegate(thisRef: Any, prop: KProperty<*>): ReadOnlyProperty<Any, T?> =
|
||||
ThreadLocalProperty(this)
|
|
@ -0,0 +1,66 @@
|
|||
package org.mariotaku.twidere.fragment.stats
|
||||
|
||||
import android.arch.lifecycle.Observer
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.fragment_account_stats.*
|
||||
import org.mariotaku.twidere.R
|
||||
import org.mariotaku.twidere.content.database.TwidereDatabase
|
||||
import org.mariotaku.twidere.content.model.AccountStats
|
||||
import org.mariotaku.twidere.data.ComputableLiveData
|
||||
import org.mariotaku.twidere.databinding.AdapterItemAccountStatCardBinding
|
||||
import org.mariotaku.twidere.fragment.BaseFragment
|
||||
import org.mariotaku.twidere.model.UserKey
|
||||
import org.mariotaku.twidere.util.Utils
|
||||
import java.util.*
|
||||
|
||||
class AccountStatsFragment : BaseFragment() {
|
||||
|
||||
private lateinit var liveStats: AccountStatsLiveData
|
||||
|
||||
private lateinit var statusesBinding: AdapterItemAccountStatCardBinding
|
||||
private lateinit var followersBinding: AdapterItemAccountStatCardBinding
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
liveStats = AccountStatsLiveData(context!!, Utils.getAccountKeys(context!!,
|
||||
arguments)!!.first())
|
||||
|
||||
|
||||
statusesBinding = AdapterItemAccountStatCardBinding.inflate(layoutInflater,
|
||||
statsScrollContent, true)
|
||||
followersBinding = AdapterItemAccountStatCardBinding.inflate(layoutInflater,
|
||||
statsScrollContent, true)
|
||||
|
||||
liveStats.observe(this, Observer {
|
||||
displaySummaries(it)
|
||||
})
|
||||
liveStats.load()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_account_stats, container, false)
|
||||
}
|
||||
|
||||
private fun displaySummaries(summaries: AccountStats.Summaries?) {
|
||||
if (summaries == null) return
|
||||
statusesBinding.statTitle.text = getString(R.string.title_statuses)
|
||||
followersBinding.statTitle.text = getString(R.string.title_followers)
|
||||
|
||||
statusesBinding.summary = summaries.statuses
|
||||
followersBinding.summary = summaries.followers
|
||||
}
|
||||
|
||||
class AccountStatsLiveData(
|
||||
val context: Context,
|
||||
val accountKey: UserKey
|
||||
) : ComputableLiveData<AccountStats.Summaries>(false) {
|
||||
override fun compute(): AccountStats.Summaries {
|
||||
return TwidereDatabase.get(context).accountDailyStats().monthlySummary(accountKey, Date())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -342,8 +342,9 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
|
|||
}
|
||||
|
||||
protected open fun onPagedListChanged(data: PagedList<ParcelableStatus>?) {
|
||||
val context = context ?: return
|
||||
val firstVisiblePosition = positionBackup.getAndSet(null) ?: return
|
||||
if (firstVisiblePosition.position == 0 && !PreferencesSingleton.get(context!!)[readFromBottomKey]) {
|
||||
if (firstVisiblePosition.position == 0 && !PreferencesSingleton.get(context)[readFromBottomKey]) {
|
||||
scrollToPositionWithOffset(0, 0)
|
||||
} else {
|
||||
scrollToPositionWithOffset(firstVisiblePosition.position, firstVisiblePosition.offset)
|
||||
|
|
|
@ -150,7 +150,8 @@ abstract class TabConfiguration {
|
|||
CustomTabType.TRENDS_SUGGESTIONS, CustomTabType.DIRECT_MESSAGES,
|
||||
CustomTabType.FAVORITES, CustomTabType.USER_TIMELINE, CustomTabType.USER_MEDIA_TIMELINE,
|
||||
CustomTabType.SEARCH_STATUSES, CustomTabType.LIST_TIMELINE,
|
||||
CustomTabType.PUBLIC_TIMELINE, CustomTabType.NETWORK_PUBLIC_TIMELINE)
|
||||
CustomTabType.PUBLIC_TIMELINE, CustomTabType.NETWORK_PUBLIC_TIMELINE,
|
||||
CustomTabType.ACCOUNT_STATS)
|
||||
|
||||
val all: List<Pair<String, TabConfiguration>> = allTypes.mapNotNull {
|
||||
val conf = ofType(it) ?: return@mapNotNull null
|
||||
|
@ -170,6 +171,7 @@ abstract class TabConfiguration {
|
|||
CustomTabType.SEARCH_STATUSES -> SearchTabConfiguration()
|
||||
CustomTabType.PUBLIC_TIMELINE -> PublicTimelineTabConfiguration()
|
||||
CustomTabType.NETWORK_PUBLIC_TIMELINE -> NetworkPublicTimelineTabConfiguration()
|
||||
CustomTabType.ACCOUNT_STATS -> AccountStatsConfiguration()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Twidere - Twitter client for Android
|
||||
*
|
||||
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.mariotaku.twidere.model.tab.impl
|
||||
|
||||
import org.mariotaku.twidere.R
|
||||
import org.mariotaku.twidere.annotation.TabAccountFlags
|
||||
import org.mariotaku.twidere.fragment.stats.AccountStatsFragment
|
||||
import org.mariotaku.twidere.model.tab.DrawableHolder
|
||||
import org.mariotaku.twidere.model.tab.StringHolder
|
||||
import org.mariotaku.twidere.model.tab.TabConfiguration
|
||||
|
||||
class AccountStatsConfiguration : TabConfiguration() {
|
||||
|
||||
override val name = StringHolder.resource(R.string.title_account_stats)
|
||||
|
||||
override val icon = DrawableHolder.Builtin.TRENDS
|
||||
|
||||
override val accountFlags = TabAccountFlags.FLAG_HAS_ACCOUNT or
|
||||
TabAccountFlags.FLAG_ACCOUNT_REQUIRED or TabAccountFlags.FLAG_ACCOUNT_MUTABLE
|
||||
|
||||
override val fragmentClass = AccountStatsFragment::class.java
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package org.mariotaku.twidere.task.worker
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import androidx.work.Worker
|
||||
import org.mariotaku.microblog.library.Mastodon
|
||||
import org.mariotaku.microblog.library.MicroBlog
|
||||
import org.mariotaku.microblog.library.MicroBlogException
|
||||
import org.mariotaku.twidere.annotation.AccountType
|
||||
import org.mariotaku.twidere.content.database.TwidereDatabase
|
||||
import org.mariotaku.twidere.content.database.dao.AccountDailyStats
|
||||
import org.mariotaku.twidere.content.model.AccountStats
|
||||
import org.mariotaku.twidere.extension.getAllDetails
|
||||
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
|
||||
import org.mariotaku.twidere.extension.model.api.toParcelable
|
||||
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class AccountDailyStatWorker : Worker() {
|
||||
|
||||
override fun doWork(): WorkerResult {
|
||||
val am = AccountManager.get(applicationContext)
|
||||
val dao = TwidereDatabase.get(applicationContext).accountDailyStats()
|
||||
val date = Date()
|
||||
val existingStats = dao.list(date)
|
||||
val accounts = am.getAllDetails(true).filterNot { account ->
|
||||
existingStats.any { it.accountKey == account.key }
|
||||
}
|
||||
val stats = accounts.mapNotNull {
|
||||
val user = try {
|
||||
when (it.type) {
|
||||
AccountType.MASTODON -> it.newMicroBlogInstance(applicationContext, Mastodon::class.java)
|
||||
.verifyCredentials().toParcelable(it.key)
|
||||
else -> it.newMicroBlogInstance(applicationContext, MicroBlog::class.java)
|
||||
.verifyCredentials().toParcelable(it.key, it.type)
|
||||
}
|
||||
} catch (e: MicroBlogException) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
return@mapNotNull AccountStats(createdAt = date, accountKey = it.key,
|
||||
statusesCount = user.statuses_count, followersCount = user.followers_count)
|
||||
}
|
||||
dao.insert(stats)
|
||||
return WorkerResult.SUCCESS
|
||||
}
|
||||
|
||||
private fun AccountDailyStats.insertTestData(stats: List<AccountStats>) {
|
||||
val cal = Calendar.getInstance()
|
||||
val date = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse("2018-05-14")
|
||||
var i = 1
|
||||
var s = stats
|
||||
while (i < 90) {
|
||||
cal.time = date
|
||||
cal.add(Calendar.DATE, -i)
|
||||
s = s.map {
|
||||
it.copy(
|
||||
rowId = 0,
|
||||
createdAt = cal.time,
|
||||
statusesCount = (it.statusesCount - Math.random() * 30).toLong().coerceAtLeast(0),
|
||||
followersCount = ((it.followersCount + Math.random() * 20).toLong() - 10).coerceAtLeast(0)
|
||||
)
|
||||
}
|
||||
insert(s)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package org.mariotaku.twidere.view.chart
|
||||
|
||||
import android.content.Context
|
||||
import android.databinding.BindingMethod
|
||||
import android.databinding.BindingMethods
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import org.mariotaku.twidere.R
|
||||
|
||||
@BindingMethods(
|
||||
BindingMethod(type = LineChartView::class, attribute = "chartValues", method = "setValues"),
|
||||
BindingMethod(type = LineChartView::class, attribute = "chartValuesCount", method = "setValuesCount")
|
||||
)
|
||||
class LineChartView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
|
||||
|
||||
var values: FloatArray? = null
|
||||
set(value) {
|
||||
field = value
|
||||
updatePath()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var valuesCount: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
updatePath()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private var chartLineColor: Int = Color.LTGRAY
|
||||
set(value) {
|
||||
field = value
|
||||
linePaint.color = value
|
||||
}
|
||||
private var chartLineSize: Float = 1f
|
||||
set(value) {
|
||||
field = value
|
||||
linePaint.strokeWidth = value
|
||||
}
|
||||
|
||||
private var chartBaseline: Float
|
||||
|
||||
private var chartBaselineColor: Int = Color.LTGRAY
|
||||
set(value) {
|
||||
field = value
|
||||
baselinePaint.color = value
|
||||
}
|
||||
private var chartBaselineSize: Float = 1f
|
||||
set(value) {
|
||||
field = value
|
||||
baselinePaint.strokeWidth = value
|
||||
}
|
||||
private var chartEndpointColor: Int = Color.LTGRAY
|
||||
set(value) {
|
||||
field = value
|
||||
endpointPaint.color = value
|
||||
}
|
||||
|
||||
private var chartEndpointSize: Float = 2f
|
||||
set(value) {
|
||||
field = value
|
||||
endpointPaint.strokeWidth = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private val linePaint = createPaint()
|
||||
private val baselinePaint = createPaint()
|
||||
private val endpointPaint = createPaint()
|
||||
|
||||
private val path: Path = Path()
|
||||
|
||||
init {
|
||||
val a = context.obtainStyledAttributes(attrs, R.styleable.LineChartView)
|
||||
chartBaseline = a.getDimension(R.styleable.LineChartView_chartBaseline, 0f)
|
||||
chartBaselineColor = a.getColor(R.styleable.LineChartView_chartBaselineColor, Color.LTGRAY)
|
||||
chartBaselineSize = a.getDimension(R.styleable.LineChartView_chartBaselineSize, 1f)
|
||||
chartLineColor = a.getColor(R.styleable.LineChartView_chartLineColor, Color.LTGRAY)
|
||||
chartLineSize = a.getDimension(R.styleable.LineChartView_chartLineSize, 1f)
|
||||
chartEndpointColor = a.getColor(R.styleable.LineChartView_chartEndpointColor, Color.DKGRAY)
|
||||
chartEndpointSize = a.getDimension(R.styleable.LineChartView_chartEndpointSize, 2f)
|
||||
a.recycle()
|
||||
}
|
||||
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.drawPath(path, linePaint)
|
||||
canvas.drawLine(0f, height - chartBaseline, width.toFloat(),
|
||||
height - chartBaseline, baselinePaint)
|
||||
|
||||
val values = this.values
|
||||
if (values != null && values.isNotEmpty()) {
|
||||
val x = width * (values.lastIndex / (valuesCount - 1f))
|
||||
val y = (height - chartBaseline) * (1 - values.last())
|
||||
canvas.drawPoint(x, y, endpointPaint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
updatePath()
|
||||
}
|
||||
|
||||
private fun updatePath() {
|
||||
if (width <= 0 || height <= 0) return
|
||||
path.reset()
|
||||
if (valuesCount <= 1) return
|
||||
val values = this.values ?: return
|
||||
values.forEachIndexed { index, value ->
|
||||
val x = width * (index / (valuesCount - 1f))
|
||||
val y = (height - chartBaseline) * (1 - value)
|
||||
if (index == 0) {
|
||||
path.moveTo(x, y)
|
||||
} else {
|
||||
path.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun createPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:bind="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="summary"
|
||||
type="org.mariotaku.twidere.content.model.AccountStats.DisplaySummary" />
|
||||
</data>
|
||||
|
||||
<android.support.v7.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="Tweets" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@{summary.numberDisplay}"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
app:layout_constraintTop_toBottomOf="@+id/statTitle"
|
||||
tools:text="335" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statGrowth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{summary.growthDisplay}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/statValue"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="+204.5%"
|
||||
tools:textColor="@color/material_light_green" />
|
||||
|
||||
<android.support.constraint.Barrier
|
||||
android:id="@+id/statBarrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="statValue,statGrowth" />
|
||||
|
||||
<org.mariotaku.twidere.view.chart.LineChartView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:chartBaseline="16dp"
|
||||
app:chartBaselineColor="#ccd6dd"
|
||||
app:chartBaselineSize="1dp"
|
||||
app:chartEndpointColor="#a6d388"
|
||||
app:chartEndpointSize="6dp"
|
||||
app:chartLineColor="#ccd6dd"
|
||||
app:chartLineSize="2dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/statBarrier"
|
||||
bind:chartValues="@{summary.values}"
|
||||
bind:chartValuesCount="@{summary.valuesCount}" />
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
</android.support.v7.widget.CardView>
|
||||
</layout>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.v4.widget.NestedScrollView
|
||||
android:id="@+id/statsScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/statsScrollContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
</android.support.v4.widget.NestedScrollView>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
|
@ -246,7 +246,7 @@
|
|||
android:baselineAligned="false"
|
||||
android:orientation="horizontal"
|
||||
android:splitMotionEvents="false"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createdAt">
|
||||
app:layout_constraintTop_toBottomOf="@+id/date">
|
||||
|
||||
<org.mariotaku.twidere.view.TwoLineTextView
|
||||
android:id="@+id/followersCount"
|
||||
|
|
|
@ -2,231 +2,240 @@
|
|||
<resources>
|
||||
|
||||
<declare-styleable name="Twidere">
|
||||
<attr name="cardActionButtonStyle" format="reference"/>
|
||||
<attr name="profileImageStyle" format="reference"/>
|
||||
<attr name="profileImageStyleLarge" format="reference"/>
|
||||
<attr name="menuIconColor" format="color"/>
|
||||
<attr name="menuIconColorDisabled" format="color"/>
|
||||
<attr name="menuIconColorActionBar" format="color"/>
|
||||
<attr name="messageBubbleColor" format="color"/>
|
||||
<attr name="cardItemBackgroundColor" format="color"/>
|
||||
<attr name="quoteIndicatorBackgroundColor" format="color"/>
|
||||
<attr name="linePageIndicatorStyle" format="reference"/>
|
||||
<attr name="mediaLabelBackground" format="color"/>
|
||||
<attr name="isDialogTheme" format="boolean"/>
|
||||
<attr name="cardActionButtonStyle" format="reference" />
|
||||
<attr name="profileImageStyle" format="reference" />
|
||||
<attr name="profileImageStyleLarge" format="reference" />
|
||||
<attr name="menuIconColor" format="color" />
|
||||
<attr name="menuIconColorDisabled" format="color" />
|
||||
<attr name="menuIconColorActionBar" format="color" />
|
||||
<attr name="messageBubbleColor" format="color" />
|
||||
<attr name="cardItemBackgroundColor" format="color" />
|
||||
<attr name="quoteIndicatorBackgroundColor" format="color" />
|
||||
<attr name="linePageIndicatorStyle" format="reference" />
|
||||
<attr name="mediaLabelBackground" format="color" />
|
||||
<attr name="isDialogTheme" format="boolean" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="TwidereTheme">
|
||||
<attr name="darkThemeResource" format="reference"/>
|
||||
<attr name="lightThemeResource" format="reference"/>
|
||||
<attr name="darkThemeResource" format="reference" />
|
||||
<attr name="lightThemeResource" format="reference" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ColorLabelView">
|
||||
<attr name="ignorePadding" format="boolean"/>
|
||||
<attr name="backgroundColor" format="color"/>
|
||||
<attr name="ignorePadding" format="boolean" />
|
||||
<attr name="backgroundColor" format="color" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="ColorPickerPreferences">
|
||||
<attr name="defaultColor" format="color"/>
|
||||
<attr name="alphaSlider" format="boolean"/>
|
||||
<attr name="defaultColor" format="color" />
|
||||
<attr name="alphaSlider" format="boolean" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="TabPagerIndicator">
|
||||
<attr name="tabStripColor" format="color"/>
|
||||
<attr name="tabIconColor" format="color"/>
|
||||
<attr name="tabLabelColor" format="color"/>
|
||||
<attr name="tabStripHeight" format="dimension"/>
|
||||
<attr name="tabHorizontalPadding" format="dimension"/>
|
||||
<attr name="tabVerticalPadding" format="dimension"/>
|
||||
<attr name="tabDividerVerticalPadding" format="dimension"/>
|
||||
<attr name="tabDividerHorizontalPadding" format="dimension"/>
|
||||
<attr name="tabExpandEnabled" format="boolean"/>
|
||||
<attr name="tabShowDivider" format="boolean"/>
|
||||
<attr name="tabDisplayOption"/>
|
||||
<attr name="tabStripColor" format="color" />
|
||||
<attr name="tabIconColor" format="color" />
|
||||
<attr name="tabLabelColor" format="color" />
|
||||
<attr name="tabStripHeight" format="dimension" />
|
||||
<attr name="tabHorizontalPadding" format="dimension" />
|
||||
<attr name="tabVerticalPadding" format="dimension" />
|
||||
<attr name="tabDividerVerticalPadding" format="dimension" />
|
||||
<attr name="tabDividerHorizontalPadding" format="dimension" />
|
||||
<attr name="tabExpandEnabled" format="boolean" />
|
||||
<attr name="tabShowDivider" format="boolean" />
|
||||
<attr name="tabDisplayOption" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="NameView">
|
||||
<attr name="nvNameFirst" format="boolean"/>
|
||||
<attr name="nvNameFirst" format="boolean" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="TwoLineTextView">
|
||||
<attr name="tltvTwoLine" format="boolean"/>
|
||||
<attr name="tltvPrimaryTextAppearance" format="reference"/>
|
||||
<attr name="tltvSecondaryTextAppearance" format="reference"/>
|
||||
<attr name="tltvPrimaryText" format="string"/>
|
||||
<attr name="tltvSecondaryText" format="string"/>
|
||||
<attr name="tltvPrimaryTextColor" format="color"/>
|
||||
<attr name="tltvSecondaryTextColor" format="color"/>
|
||||
<attr name="tltvPrimaryLinkTextColor" format="color"/>
|
||||
<attr name="tltvSecondaryLinkTextColor" format="color"/>
|
||||
<attr name="tltvPrimaryTextStyle"/>
|
||||
<attr name="tltvSecondaryTextStyle"/>
|
||||
<attr name="tltvPrimaryTextSize" format="dimension"/>
|
||||
<attr name="tltvSecondaryTextSize" format="dimension"/>
|
||||
<attr name="tltvTwoLine" format="boolean" />
|
||||
<attr name="tltvPrimaryTextAppearance" format="reference" />
|
||||
<attr name="tltvSecondaryTextAppearance" format="reference" />
|
||||
<attr name="tltvPrimaryText" format="string" />
|
||||
<attr name="tltvSecondaryText" format="string" />
|
||||
<attr name="tltvPrimaryTextColor" format="color" />
|
||||
<attr name="tltvSecondaryTextColor" format="color" />
|
||||
<attr name="tltvPrimaryLinkTextColor" format="color" />
|
||||
<attr name="tltvSecondaryLinkTextColor" format="color" />
|
||||
<attr name="tltvPrimaryTextStyle" />
|
||||
<attr name="tltvSecondaryTextStyle" />
|
||||
<attr name="tltvPrimaryTextSize" format="dimension" />
|
||||
<attr name="tltvSecondaryTextSize" format="dimension" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="ValueDependencyPreference">
|
||||
<attr name="dependencyKey" format="string"/>
|
||||
<attr name="dependencyValues" format="reference"/>
|
||||
<attr name="dependencyValueDefault" format="string"/>
|
||||
<attr name="dependencyKey" format="string" />
|
||||
<attr name="dependencyValues" format="reference" />
|
||||
<attr name="dependencyValueDefault" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="AccountsListPreference">
|
||||
<attr name="switchKey" format="string"/>
|
||||
<attr name="switchDefault" format="boolean"/>
|
||||
<attr name="switchKey" format="string" />
|
||||
<attr name="switchDefault" format="boolean" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="NotificationTypePreference">
|
||||
<attr name="notificationType"/>
|
||||
<attr name="notificationType" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="SeekBarDialogPreference">
|
||||
<attr name="max" format="integer"/>
|
||||
<attr name="min" format="integer"/>
|
||||
<attr name="step" format="integer"/>
|
||||
<attr name="progressTextSuffix" format="string"/>
|
||||
<attr name="max" format="integer" />
|
||||
<attr name="min" format="integer" />
|
||||
<attr name="step" format="integer" />
|
||||
<attr name="progressTextSuffix" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="KeyboardShortcutPreference">
|
||||
<attr name="android:action"/>
|
||||
<attr name="android:tag"/>
|
||||
<attr name="android:action" />
|
||||
<attr name="android:tag" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="SyncItemPreference">
|
||||
<attr name="syncType" format="string"/>
|
||||
<attr name="syncType" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="DrawableTintTextView">
|
||||
<attr name="drawableTint" format="reference"/>
|
||||
<attr name="drawableTint" format="reference" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="ShapedImageView">
|
||||
<attr name="sivBorder" format="boolean"/>
|
||||
<attr name="sivBorderWidth" format="dimension"/>
|
||||
<attr name="sivBorderColor" format="color"/>
|
||||
<attr name="sivBackgroundColor" format="color"/>
|
||||
<attr name="sivElevation" format="dimension"/>
|
||||
<attr name="sivCornerRadius" format="dimension"/>
|
||||
<attr name="sivCornerRadiusRatio" format="fraction"/>
|
||||
<attr name="sivDrawShadow" format="boolean"/>
|
||||
<attr name="sivShape"/>
|
||||
<attr name="sivBorder" format="boolean" />
|
||||
<attr name="sivBorderWidth" format="dimension" />
|
||||
<attr name="sivBorderColor" format="color" />
|
||||
<attr name="sivBackgroundColor" format="color" />
|
||||
<attr name="sivElevation" format="dimension" />
|
||||
<attr name="sivCornerRadius" format="dimension" />
|
||||
<attr name="sivCornerRadiusRatio" format="fraction" />
|
||||
<attr name="sivDrawShadow" format="boolean" />
|
||||
<attr name="sivShape" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="TintedStatusLayout">
|
||||
<attr name="setPadding" format="boolean"/>
|
||||
<attr name="setPadding" format="boolean" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="BadgeView">
|
||||
<attr name="android:textColor"/>
|
||||
<attr name="android:text"/>
|
||||
<attr name="android:textColor" />
|
||||
<attr name="android:text" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="CardMediaContainer">
|
||||
<attr name="android:horizontalSpacing"/>
|
||||
<attr name="android:verticalSpacing"/>
|
||||
<attr name="android:layout"/>
|
||||
<attr name="android:horizontalSpacing" />
|
||||
<attr name="android:verticalSpacing" />
|
||||
<attr name="android:layout" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="MessageViewHolder">
|
||||
<attr name="android:textColorPrimary"/>
|
||||
<attr name="android:textColorPrimaryInverse"/>
|
||||
<attr name="android:textColorSecondary"/>
|
||||
<attr name="android:textColorSecondaryInverse"/>
|
||||
<attr name="android:textColorPrimary" />
|
||||
<attr name="android:textColorPrimaryInverse" />
|
||||
<attr name="android:textColorSecondary" />
|
||||
<attr name="android:textColorSecondaryInverse" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="RingtonePreference">
|
||||
<attr name="android:ringtoneType"/>
|
||||
<attr name="android:showDefault"/>
|
||||
<attr name="android:showSilent"/>
|
||||
<attr name="android:ringtoneType" />
|
||||
<attr name="android:showDefault" />
|
||||
<attr name="android:showSilent" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="ForegroundView">
|
||||
<attr name="android:foreground"/>
|
||||
<attr name="android:foregroundGravity"/>
|
||||
<attr name="android:foreground" />
|
||||
<attr name="android:foregroundGravity" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="AccountDashboardHeaderView">
|
||||
<attr name="sizeMeasurementId" format="reference"/>
|
||||
<attr name="sizeMeasurementId" format="reference" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="PremiumEntryPreference">
|
||||
<attr name="requiredFeature" format="string"/>
|
||||
<attr name="requiredFeature" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="ProfileBannerSpace">
|
||||
<attr name="bannerAspectRatio"/>
|
||||
<attr name="bannerAspectRatio" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="ProfileBannerImageView">
|
||||
<attr name="bannerAspectRatio"/>
|
||||
<attr name="bannerAspectRatio" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="AppBarChildBehavior">
|
||||
<attr name="behavior_appBarId" format="reference"/>
|
||||
<attr name="behavior_toolbarId" format="reference"/>
|
||||
<attr name="behavior_dependencyViewId" format="reference"/>
|
||||
<attr name="behavior_targetViewId" format="reference"/>
|
||||
<attr name="behavior_appBarId" format="reference" />
|
||||
<attr name="behavior_toolbarId" format="reference" />
|
||||
<attr name="behavior_dependencyViewId" format="reference" />
|
||||
<attr name="behavior_targetViewId" format="reference" />
|
||||
|
||||
<attr name="behavior_marginLeft" format="dimension"/>
|
||||
<attr name="behavior_marginRight" format="dimension"/>
|
||||
<attr name="behavior_marginStart" format="dimension"/>
|
||||
<attr name="behavior_marginEnd" format="dimension"/>
|
||||
<attr name="behavior_marginTop" format="dimension"/>
|
||||
<attr name="behavior_marginBottom" format="dimension"/>
|
||||
<attr name="behavior_marginLeft" format="dimension" />
|
||||
<attr name="behavior_marginRight" format="dimension" />
|
||||
<attr name="behavior_marginStart" format="dimension" />
|
||||
<attr name="behavior_marginEnd" format="dimension" />
|
||||
<attr name="behavior_marginTop" format="dimension" />
|
||||
<attr name="behavior_marginBottom" format="dimension" />
|
||||
|
||||
<attr name="behavior_alignmentRule"/>
|
||||
<attr name="behavior_alignmentRule" />
|
||||
|
||||
<attr name="behavior_childTransformation" format="string"/>
|
||||
<attr name="behavior_childTransformation" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="IExtendedViewPadding">
|
||||
<attr name="android:paddingTop"/>
|
||||
<attr name="android:paddingBottom"/>
|
||||
<attr name="android:paddingLeft"/>
|
||||
<attr name="android:paddingRight"/>
|
||||
<attr name="android:paddingStart"/>
|
||||
<attr name="android:paddingEnd"/>
|
||||
<attr name="android:padding"/>
|
||||
<attr name="android:paddingTop" />
|
||||
<attr name="android:paddingBottom" />
|
||||
<attr name="android:paddingLeft" />
|
||||
<attr name="android:paddingRight" />
|
||||
<attr name="android:paddingStart" />
|
||||
<attr name="android:paddingEnd" />
|
||||
<attr name="android:padding" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="MediaLayoutParams">
|
||||
<attr name="layout_isMediaItemView" format="boolean"/>
|
||||
<attr name="layout_videoViewId" format="reference"/>
|
||||
<attr name="layout_isMediaItemView" format="boolean" />
|
||||
<attr name="layout_videoViewId" format="reference" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="MaxHeightScrollView">
|
||||
<attr name="android:maxHeight"/>
|
||||
<attr name="android:maxHeight" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="LabeledImageButton">
|
||||
<attr name="android:text"/>
|
||||
<attr name="android:textAppearance"/>
|
||||
<attr name="android:textColor"/>
|
||||
<attr name="android:textSize"/>
|
||||
<attr name="android:textStyle"/>
|
||||
<attr name="android:drawablePadding"/>
|
||||
<attr name="android:text" />
|
||||
<attr name="android:textAppearance" />
|
||||
<attr name="android:textColor" />
|
||||
<attr name="android:textSize" />
|
||||
<attr name="android:textStyle" />
|
||||
<attr name="android:drawablePadding" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="BannerBehavior">
|
||||
<attr name="bannerAspectRatio"/>
|
||||
<attr name="bannerAspectRatio" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="LineChartView">
|
||||
<attr name="chartBaseline" format="dimension" />
|
||||
<attr name="chartBaselineColor" format="color" />
|
||||
<attr name="chartLineColor" format="color" />
|
||||
<attr name="chartLineSize" format="dimension" />
|
||||
<attr name="chartBaselineSize" format="dimension" />
|
||||
<attr name="chartEndpointColor" format="color" />
|
||||
<attr name="chartEndpointSize" format="dimension" />
|
||||
</declare-styleable>
|
||||
<attr name="behavior_alignmentRule">
|
||||
<flag name="left" value="0x00000001"/>
|
||||
<flag name="right" value="0x00000002"/>
|
||||
<flag name="toLeftOf" value="0x00000004"/>
|
||||
<flag name="toRightOf" value="0x00000008"/>
|
||||
<flag name="toLeftOfCenter" value="0x00000010"/>
|
||||
<flag name="toRightOfCenter" value="0x00000020"/>
|
||||
<flag name="centerHorizontal" value="0x00000003"/>
|
||||
<flag name="start" value="0x00001001"/>
|
||||
<flag name="end" value="0x00001002"/>
|
||||
<flag name="toStartOf" value="0x00001004"/>
|
||||
<flag name="toEndOf" value="0x00001008"/>
|
||||
<flag name="toStartOfCenter" value="0x00001010"/>
|
||||
<flag name="toEndOfCenter" value="0x00001020"/>
|
||||
<flag name="top" value="0x00010000"/>
|
||||
<flag name="bottom" value="0x00020000"/>
|
||||
<flag name="above" value="0x00040000"/>
|
||||
<flag name="below" value="0x00080000"/>
|
||||
<flag name="aboveCenter" value="0x00100000"/>
|
||||
<flag name="belowCenter" value="0x00200000"/>
|
||||
<flag name="centerVertical" value="0x00030000"/>
|
||||
<flag name="center" value="0x00030003"/>
|
||||
<flag name="left" value="0x00000001" />
|
||||
<flag name="right" value="0x00000002" />
|
||||
<flag name="toLeftOf" value="0x00000004" />
|
||||
<flag name="toRightOf" value="0x00000008" />
|
||||
<flag name="toLeftOfCenter" value="0x00000010" />
|
||||
<flag name="toRightOfCenter" value="0x00000020" />
|
||||
<flag name="centerHorizontal" value="0x00000003" />
|
||||
<flag name="start" value="0x00001001" />
|
||||
<flag name="end" value="0x00001002" />
|
||||
<flag name="toStartOf" value="0x00001004" />
|
||||
<flag name="toEndOf" value="0x00001008" />
|
||||
<flag name="toStartOfCenter" value="0x00001010" />
|
||||
<flag name="toEndOfCenter" value="0x00001020" />
|
||||
<flag name="top" value="0x00010000" />
|
||||
<flag name="bottom" value="0x00020000" />
|
||||
<flag name="above" value="0x00040000" />
|
||||
<flag name="below" value="0x00080000" />
|
||||
<flag name="aboveCenter" value="0x00100000" />
|
||||
<flag name="belowCenter" value="0x00200000" />
|
||||
<flag name="centerVertical" value="0x00030000" />
|
||||
<flag name="center" value="0x00030003" />
|
||||
</attr>
|
||||
<attr name="notificationType">
|
||||
<flag name="none" value="0"/>
|
||||
<flag name="ringtone" value="1"/>
|
||||
<flag name="vibration" value="2"/>
|
||||
<flag name="light" value="4"/>
|
||||
<flag name="none" value="0" />
|
||||
<flag name="ringtone" value="1" />
|
||||
<flag name="vibration" value="2" />
|
||||
<flag name="light" value="4" />
|
||||
</attr>
|
||||
<attr name="tabDisplayOption">
|
||||
<flag name="label" value="0x1"/>
|
||||
<flag name="icon" value="0x2"/>
|
||||
<flag name="label" value="0x1" />
|
||||
<flag name="icon" value="0x2" />
|
||||
</attr>
|
||||
<attr name="sivShape">
|
||||
<enum name="circle" value="0x1"/>
|
||||
<enum name="rectangle" value="0x2"/>
|
||||
<enum name="circle" value="0x1" />
|
||||
<enum name="rectangle" value="0x2" />
|
||||
</attr>
|
||||
<attr name="tltvPrimaryTextStyle">
|
||||
<flag name="normal" value="0"/>
|
||||
<flag name="bold" value="1"/>
|
||||
<flag name="italic" value="2"/>
|
||||
<flag name="normal" value="0" />
|
||||
<flag name="bold" value="1" />
|
||||
<flag name="italic" value="2" />
|
||||
</attr>
|
||||
<attr name="tltvSecondaryTextStyle">
|
||||
<flag name="normal" value="0"/>
|
||||
<flag name="bold" value="1"/>
|
||||
<flag name="italic" value="2"/>
|
||||
<flag name="normal" value="0" />
|
||||
<flag name="bold" value="1" />
|
||||
<flag name="italic" value="2" />
|
||||
</attr>
|
||||
<attr name="bannerAspectRatio" format="fraction"/>
|
||||
<attr name="bannerAspectRatio" format="fraction" />
|
||||
</resources>
|
|
@ -1316,6 +1316,7 @@
|
|||
<string name="translators">Translators</string>
|
||||
|
||||
<string name="trends">Trends</string>
|
||||
<string name="title_account_stats">Stats</string>
|
||||
<string name="trends_location">Trends location</string>
|
||||
<string name="trends_location_summary">Set location for local trends.</string>
|
||||
|
||||
|
|
Loading…
Reference in New Issue