Skip to content

Commit

Permalink
Merge pull request #181 from alexandr7035/develop
Browse files Browse the repository at this point in the history
Release v5.0.

- Implement contributions grid (Github-like)
- Bug fixes
  • Loading branch information
alexandr7035 authored Nov 16, 2021
2 parents 1520c79 + 947b89e commit b8867d8
Show file tree
Hide file tree
Showing 43 changed files with 877 additions and 62 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ GitStat is a simple android app designed to aggregate Github profile data into i
- List of your repositories with filters and sorting
- Contributions summary
- Charts for contributions (types, count per day, contribution rate)
- Contributions grid (Github-like)

[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height=100
alt="Download from Google Play">](https://play.google.com/store/apps/details?id=by.alexandr7035.gitstat)
Expand All @@ -41,15 +42,17 @@ used to implement languages filter (see third screenshot).
## Screenshots

<p align="left">
<img src="doc/screenshot_login.png" width="30%"/>
<img src="doc/screenshot_oauth.png" width="30%"/>
<img src="doc/screenshot_repositories_stat.png" width="30%"/>
<img src="doc/screenshot_login.png" width="23%"/>
<img src="doc/screenshot_oauth.png" width="23%"/>
<img src="doc/screenshot_profile.png" width="23%"/>
<img src="doc/screenshot_repositories_stat.png" width="23%"/>
</p>

<p align="left">
<img src="doc/screenshot_filters.png" width="30%"/>
<img src="doc/screenshot_contributions_1.png" width="30%"/>
<img src="doc/screenshot_contributions_2.png" width="30%"/>
<img src="doc/screenshot_filters.png" width="23%"/>
<img src="doc/screenshot_contributions_1.png" width="23%"/>
<img src="doc/screenshot_contributions_2.png" width="23%"/>
<img src="doc/screenshot_contributions_grid_1.png" width="23%"/>
</p>

## Privacy Policy
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ android {
applicationId 'by.alexandr7035.gitstat'
minSdkVersion 21
targetSdkVersion 31
versionCode 1700
versionName "4.5.4"
versionCode 1800
versionName "5.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ query Contributions($date_from: DateTime, $date_to: DateTime) {
contributionDays {
contributionCount
date
color
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/by/alexandr7035/gitstat/core/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.app.NotificationManager
import android.os.Build
import by.alexandr7035.gitstat.BuildConfig
import by.alexandr7035.gitstat.R
import by.alexandr7035.gitstat.data.AppPreferences
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber

Expand All @@ -31,5 +32,14 @@ class App : Application() {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}


// Force cache (re)synchronization on start if new version was installed
// Cache DB schema may be changed and some fields may become 0
val appPrefs = AppPreferences(this)
if (appPrefs.getLastInstalledVersionCode() != BuildConfig.VERSION_CODE) {
appPrefs.saveLastInstalledVersionCode(BuildConfig.VERSION_CODE)
appPrefs.saveLastCacheSyncDate(0)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package by.alexandr7035.gitstat.core

import android.content.Context
import android.util.AttributeSet
import android.view.View

// Use it with one side with WRAP_CONTENT
class ContributionCellView: View {
constructor(context: Context): super(context)
constructor(context: Context, attrs: AttributeSet): super(context, attrs)

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)

val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)

if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
setMeasuredDimension(widthSize, widthSize)
}
else if(heightMode == MeasureSpec.EXACTLY && widthMode != MeasureSpec.EXACTLY) {
setMeasuredDimension(heightSize, heightSize)
}
else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ interface KeyValueStorage {
fun getRepositoriesFilters(): String?

fun saveRepositoriesFilters(filtersStr: String?)

fun getLastInstalledVersionCode(): Int

fun saveLastInstalledVersionCode(versionCode: Int)
}
13 changes: 13 additions & 0 deletions app/src/main/java/by/alexandr7035/gitstat/core/TimeHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ class TimeHelper {
return getUnixDateFrom_yyyyMMdd(beginningStr)
}

fun getBeginningOfDayForUnixDate_currentTz(currentDate: Long): Long {
val format = SimpleDateFormat("yyyy-MM-dd", Locale.US)
val beginningStr = format.format(currentDate)

return getUnixDateFrom_yyyyMMdd(beginningStr)
}

fun getCurrentYearForUnixDate(currentDate: Long): Int {
val format = SimpleDateFormat("yyyy", Locale.US)
format.timeZone = TimeZone.getTimeZone("GMT")
Expand All @@ -53,5 +60,11 @@ class TimeHelper {
return yearStr.toInt()
}

fun get_yyyyMM_fromUnixDate(unixDate: Long): String {
val format = SimpleDateFormat("yyyyMM", Locale.US)
format.timeZone = TimeZone.getTimeZone("GMT")
return format.format(unixDate)
}

data class Iso8601Year(val startDate: String, val endDate: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,12 @@ class AppPreferences @Inject constructor(private val application: Application):
override fun saveRepositoriesFilters(filtersStr: String?) {
prefs.edit().putString(application.getString(R.string.shared_prefs_filters), filtersStr).apply()
}

override fun getLastInstalledVersionCode(): Int {
return prefs.getInt(application.getString(R.string.shared_pref_last_version_code), 0)
}

override fun saveLastInstalledVersionCode(versionCode: Int) {
prefs.edit().putInt(application.getString(R.string.shared_pref_last_version_code), versionCode).apply()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import androidx.lifecycle.LiveData
import by.alexandr7035.gitstat.core.KeyValueStorage
import by.alexandr7035.gitstat.core.TimeHelper
import by.alexandr7035.gitstat.data.local.dao.ContributionsDao
import by.alexandr7035.gitstat.data.local.model.ContributionDayEntity
import by.alexandr7035.gitstat.data.local.model.ContributionTypesEntity
import by.alexandr7035.gitstat.data.local.model.ContributionsYearWithDays
import by.alexandr7035.gitstat.data.local.model.ContributionsYearWithRates
import by.alexandr7035.gitstat.data.local.model.*
import javax.inject.Inject
import kotlin.math.round

Expand All @@ -30,6 +27,10 @@ class ContributionsRepository @Inject constructor(
return dao.getContributionYearsWithRatesCache()
}

fun getContributionYearsWithMonthsLiveData(): LiveData<List<ContributionYearWithMonths>> {
return dao.getContributionYearsWithMonthsLiveData()
}

fun getContributionTypesLiveData(): LiveData<List<ContributionTypesEntity>> {
return dao.getContributionTypesLiveData()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import by.alexandr7035.gitstat.data.remote.mappers.*
import by.alexandr7035.gitstat.extensions.performRequestWithDataResult
import com.apollographql.apollo3.ApolloClient
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList

class DataSyncRepository @Inject constructor(
private val apolloClient: ApolloClient,
Expand Down Expand Up @@ -44,10 +46,11 @@ class DataSyncRepository @Inject constructor(
val repositories = fetchRepositories()

syncStatusLiveData?.postValue(DataSyncStatus.PENDING_CONTRIBUTIONS)
val contributionYears = fetchContributionYears(accountCreationYear, currentYear)
val contributionDays = fetchContributionDays(accountCreationYear, currentYear)
val contributionTypes = fetchContributionTypes(accountCreationYear, currentYear, contributionDays)
val contributionRates = fetchContributionRates(contributionDays)
val contributionYears = fetchContributionYears(accountCreationYear, currentYear)
val contributionMonths = fetchContributionMonths(contributionDays)

// Write cache
clearCache()
Expand All @@ -64,12 +67,13 @@ class DataSyncRepository @Inject constructor(
// NOTE! DO NOT CHANGE ORDER
// LIVEDATA TRIGGERED IMMEDIATELY AFTER SAVING CACHE
// AS ONE DATA MAY DEPEND ON THE OTHER, WRONG ORDER MAY CAUSE CRASH
// Years must be at the end
// Years and months must be at the end
db.getContributionsDao().apply {
insertContributionRatesCache(contributionRates)
insertContributionDays(contributionDays)
insertContributionTypes(contributionTypes)
insertContributionYearsCache(contributionYears)
insertContributionMonthsCache(contributionMonths)
}

syncStatusLiveData?.postValue(DataSyncStatus.SUCCESS)
Expand Down Expand Up @@ -118,6 +122,23 @@ class DataSyncRepository @Inject constructor(
return years
}

private fun fetchContributionMonths(contributionDays: List<ContributionDayEntity>): ArrayList<ContributionsMonthEntity> {

// Use TreeMap as it is always sorted by key
val monthsMap: TreeMap<Int, Int> = TreeMap()
val months = ArrayList<ContributionsMonthEntity>()

for (day in contributionDays) {
monthsMap[day.monthId] = day.yearId
}

for ((id, yearId) in monthsMap) {
months.add(ContributionsMonthEntity(id, yearId))
}

return months
}

private suspend fun fetchContributionDays(profileCreationYear: Int, currentYear: Int): List<ContributionDayEntity> {

val contributionDaysCached = ArrayList<ContributionDayEntity>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import by.alexandr7035.gitstat.data.local.model.*
ContributionDayEntity::class,
ContributionRateEntity::class,
ContributionsYearEntity::class,
ContributionTypesEntity::class], version = 16)
ContributionTypesEntity::class,
ContributionsMonthEntity::class], version = 20)

abstract class CacheDB : RoomDatabase() {

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
package by.alexandr7035.gitstat.data.local.daoimport androidx.lifecycle.LiveDataimport androidx.room.Daoimport androidx.room.Insertimport androidx.room.OnConflictStrategyimport androidx.room.Queryimport by.alexandr7035.gitstat.data.local.model.*@Daointerface ContributionsDao { ///////////////////////////////////// // Contribution days ///////////////////////////////////// @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertContributionDays(contributions: List<ContributionDayEntity>) @Query("select * from contributions order by date") fun getContributionDaysLiveData(): LiveData<List<ContributionDayEntity>> @Query("select * from contributions order by date asc") fun getContributionDaysList(): List<ContributionDayEntity> @Query("DELETE FROM contributions") suspend fun clearContributionDays() ///////////////////////////////////// // Contribution years ///////////////////////////////////// @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertContributionYearsCache(years: List<ContributionsYearEntity>) @Query("SELECT * FROM contribution_years") fun getContributionYearsWithDaysLiveData(): LiveData<List<ContributionsYearWithDays>> @Query("SELECT * FROM contribution_years where id = (:yearId)") suspend fun getContributionYearWithDays(yearId: Int): ContributionsYearWithDays @Query("DELETE FROM contribution_years") fun clearContributionYears() ///////////////////////////////////// // Contribution rates ///////////////////////////////////// @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertContributionRatesCache(rates: List<ContributionRateEntity>) @Query("SELECT * FROM contribution_years") fun getContributionYearsWithRatesCache(): LiveData<List<ContributionsYearWithRates>> @Query("DELETE FROM contribution_rates") suspend fun clearContributionsYearsWithRatesCache() @Query("SELECT * FROM contribution_rates") suspend fun getContributionYearWithRates(): ContributionsYearWithRates ///////////////////////////////////// // Contribution types (issues, created repos, PRs, commits, code reviews, unknown) ///////////////////////////////////// @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertContributionTypes(typesData: List<ContributionTypesEntity>) @Query("select * from contribution_types") fun getContributionTypesLiveData(): LiveData<List<ContributionTypesEntity>> @Query("DELETE FROM contribution_types") suspend fun clearContributionTypes()}
package by.alexandr7035.gitstat.data.local.daoimport androidx.lifecycle.LiveDataimport androidx.room.Daoimport androidx.room.Insertimport androidx.room.OnConflictStrategyimport androidx.room.Queryimport by.alexandr7035.gitstat.data.local.model.*@Daointerface ContributionsDao { ///////////////////////////////////// // Contribution days ///////////////////////////////////// @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertContributionDays(contributions: List<ContributionDayEntity>) @Query("select * from contributions order by date") fun getContributionDaysLiveData(): LiveData<List<ContributionDayEntity>> @Query("select * from contributions order by date asc") fun getContributionDaysList(): List<ContributionDayEntity> @Query("DELETE FROM contributions") suspend fun clearContributionDays() ///////////////////////////////////// // Contribution years and months // Use for convenience to avoid calculations and iterations in UI layer ///////////////////////////////////// @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertContributionYearsCache(years: List<ContributionsYearEntity>) @Query("SELECT * FROM contribution_years") fun getContributionYearsWithDaysLiveData(): LiveData<List<ContributionsYearWithDays>> @Query("SELECT * FROM contribution_years where id = (:yearId)") suspend fun getContributionYearWithDays(yearId: Int): ContributionsYearWithDays @Query("DELETE FROM contribution_years") fun clearContributionYears() @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertContributionMonthsCache(months: List<ContributionsMonthEntity>) @Query("SELECT * FROM contribution_years where id = (:yearId)") fun getContributionYearWithMonthsLiveData(yearId: Int): LiveData<ContributionYearWithMonths> @Query("SELECT * FROM contribution_years") fun getContributionYearsWithMonthsLiveData(): LiveData<List<ContributionYearWithMonths>> ///////////////////////////////////// // Contribution rates ///////////////////////////////////// @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertContributionRatesCache(rates: List<ContributionRateEntity>) @Query("SELECT * FROM contribution_years") fun getContributionYearsWithRatesCache(): LiveData<List<ContributionsYearWithRates>> @Query("DELETE FROM contribution_rates") suspend fun clearContributionsYearsWithRatesCache() @Query("SELECT * FROM contribution_rates") suspend fun getContributionYearWithRates(): ContributionsYearWithRates ///////////////////////////////////// // Contribution types (issues, created repos, PRs, commits, code reviews, unknown) ///////////////////////////////////// @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertContributionTypes(typesData: List<ContributionTypesEntity>) @Query("select * from contribution_types") fun getContributionTypesLiveData(): LiveData<List<ContributionTypesEntity>> @Query("DELETE FROM contribution_types") suspend fun clearContributionTypes()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ class ContributionDayEntity(
var id: Int = 0,
var date: Long,
var count: Int,
var yearId: Int)
val color: Int,

var yearId: Int,
var monthId: Int)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package by.alexandr7035.gitstat.data.local.model

import androidx.room.Embedded
import androidx.room.Relation

class ContributionYearWithMonths(
@Embedded
val year: ContributionsYearEntity,

@Relation(parentColumn = "id", entityColumn = "yearId", entity = ContributionsMonthEntity::class)
val contributionMonths: List<ContributionsMonthWithDays>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package by.alexandr7035.gitstat.data.local.model

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "contribution_months")
data class ContributionsMonthEntity(
// Save here "year"+ "month number".
// E.g. 2020 + 01 -> 202001
@PrimaryKey
val id: Int,
val yearId: Int
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package by.alexandr7035.gitstat.data.local.model

import androidx.room.Embedded
import androidx.room.Relation

class ContributionsMonthWithDays(
@Embedded
val month: ContributionsMonthEntity,

@Relation(parentColumn = "id", entityColumn = "monthId", entity = ContributionDayEntity::class)
val contributionDays: List<ContributionDayEntity>)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package by.alexandr7035.gitstat.data.remote.mappers

import android.graphics.Color
import by.alexandr7035.gitstat.apollo.ContributionsQuery
import by.alexandr7035.gitstat.core.Mapper
import by.alexandr7035.gitstat.core.TimeHelper
Expand All @@ -12,8 +13,17 @@ class ContributionDayRemoteToCacheMapper @Inject constructor(private val timeHel

val unixDate = timeHelper.getUnixDateFrom_yyyyMMdd(dateStr)
val year = timeHelper.getYearFromUnixDate(unixDate)
val month = timeHelper.get_yyyyMM_fromUnixDate(unixDate).toInt()

return ContributionDayEntity(count = data.contributionCount, date = unixDate, yearId = year)
val intColor = Color.parseColor(data.color)

return ContributionDayEntity(
count = data.contributionCount,
date = unixDate,
yearId = year,
color = intColor,
monthId = month
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ class ContributionsFragment : Fragment() {
private var binding: FragmentContributionsBinding? = null
private val viewModel by viewModels<ContributionsViewModel>()

// private lateinit var yearContributionsAdapter: YearContributionsAdapter
// private lateinit var yearContributionsRateAdapter: YearContributionRatesAdapter

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentContributionsBinding.inflate(inflater, container, false)
return binding!!.root
Expand Down Expand Up @@ -179,6 +176,11 @@ class ContributionsFragment : Fragment() {
getString(R.string.contribution_rate_dynamics_help_text)
))
}


binding?.toContributionsGridBtn?.setOnClickListener {
findNavController().navigateSafe(ContributionsFragmentDirections.actionContributionsFragmentToContributionsGridFragment(2021))
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package by.alexandr7035.gitstat.view.contributions.plots.contributions_per_year

import android.annotation.SuppressLint
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
Expand All @@ -10,8 +11,10 @@ class YearContributionsAdapter(fragment: Fragment): FragmentStateAdapter(fragmen

private var items: List<ContributionsYearWithDays> = emptyList()

@SuppressLint("NotifyDataSetChanged")
fun setItems(years: List<ContributionsYearWithDays>) {
this.items = years
notifyDataSetChanged()
}

override fun getItemCount(): Int {
Expand Down
Loading

0 comments on commit b8867d8

Please sign in to comment.