diff --git a/app/build.gradle b/app/build.gradle index 9a179691..6289ed69 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,8 +32,8 @@ android { applicationId 'by.alexandr7035.gitstat' minSdkVersion 21 targetSdkVersion 31 - versionCode 1800 - versionName "5.0" + versionCode 1900 + versionName "5.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/graphql/com/alexandr7035/gitstat/Repositories.graphql b/app/src/main/graphql/com/alexandr7035/gitstat/Repositories.graphql index 387a6974..14866d63 100644 --- a/app/src/main/graphql/com/alexandr7035/gitstat/Repositories.graphql +++ b/app/src/main/graphql/com/alexandr7035/gitstat/Repositories.graphql @@ -5,12 +5,34 @@ query Repositories { nodes { databaseId name + nameWithOwner + + description + homepageUrl + + repositoryTopics(first: 100) { + nodes { + topic { + name + } + } + } primaryLanguage { color name } + languages(first: 100) { + edges { + node { + name + color + } + size + } + } + isPrivate isArchived isFork @@ -18,6 +40,12 @@ query Repositories { stargazerCount createdAt + updatedAt + diskUsage + + parent { + nameWithOwner + } } } } diff --git a/app/src/main/java/by/alexandr7035/gitstat/core/view/HorizontalRatioBarView.kt b/app/src/main/java/by/alexandr7035/gitstat/core/view/HorizontalRatioBarView.kt new file mode 100644 index 00000000..7070af7d --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/core/view/HorizontalRatioBarView.kt @@ -0,0 +1,95 @@ +package by.alexandr7035.gitstat.core.view + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import by.alexandr7035.gitstat.R +import by.alexandr7035.gitstat.extensions.debug +import timber.log.Timber + + +class HorizontalRatioBarView(context: Context, private val attrs: AttributeSet) : View(context, attrs) { + + private val paint = Paint() + + private var colors: List? = null + private var values: List? = null + + private var spacingBetweenEntries = 0f + private var entryCornerRadius = 0f + + init { + val typedArray = context.theme.obtainStyledAttributes( + attrs, R.styleable.HorizontalRatioBarView, 0, 0 + ) + + spacingBetweenEntries = typedArray.getDimension(R.styleable.HorizontalRatioBarView_hrb_entries_between_spacing, 0f) + entryCornerRadius = typedArray.getDimensionPixelSize(R.styleable.HorizontalRatioBarView_hrb_entry_corner_radius, 0).toFloat() + Timber.debug("customView onAttachToWindow() spacing $spacingBetweenEntries corners $entryCornerRadius") + + typedArray.recycle() + } + + + @SuppressLint("DrawAllocation") + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + Timber.debug("customView onDraw()") + + if ((! colors.isNullOrEmpty()) && (! values.isNullOrEmpty())) { + + // Total sum of values + // Used to calculate percentage for a particular value + val total = values!!.sum() + + // View width without spacings + val realWidth = width - spacingBetweenEntries * (values!!.size - 1) + + // Inset is changed for next value + var inset = 0f + + values!!.forEachIndexed { index, bar -> + + // Calculate value percentage + val valuePercentage = bar / total + + val rectLeft = inset + val rectTop = 0f + // Value pixel size considering percentage and width of view + val rectRight = inset + valuePercentage*realWidth + val rectBottom = height.toFloat() + + // Set color + paint.color = colors!![index] + + + // Draw entry + val rect = RectF(rectLeft, rectTop, rectRight, rectBottom) + canvas.drawRoundRect( + rect, + entryCornerRadius, + entryCornerRadius, + paint + ) + + inset += rect.width() + inset += spacingBetweenEntries + } + } + + } + + fun setValues(values: List, colors: List) { + + if (values.size != colors.size) { + throw IllegalArgumentException("Lengths of values and colors lists MUST be the same!") + } + + this.values = values + this.colors = colors + } +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/data/ReposRepository.kt b/app/src/main/java/by/alexandr7035/gitstat/data/ReposRepository.kt index db273eb7..e8527730 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/data/ReposRepository.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/data/ReposRepository.kt @@ -18,6 +18,10 @@ class ReposRepository @Inject constructor( return dao.getRepositoriesLiveData() } + fun getRepositoryLiveData(repositoryId: Int): LiveData { + return dao.getRepositoryLiveData(repositoryId) + } + suspend fun fetchAllRepositoriesFromDb(): List { return dao.getRepositories() } @@ -57,14 +61,14 @@ class ReposRepository @Inject constructor( val languagesList = ArrayList() repos.forEach { - languagesList.add(Language(it.language, it.languageColor, 0)) + languagesList.add(Language(it.primaryLanguage, it.primaryLanguageColor, 0)) } val trimmedLanguages = languagesList.distinct() repos.forEach { repo -> trimmedLanguages.forEach { language -> - if (repo.language == language.name) { + if (repo.primaryLanguage == language.name) { language.count += 1 } } diff --git a/app/src/main/java/by/alexandr7035/gitstat/data/local/CacheDB.kt b/app/src/main/java/by/alexandr7035/gitstat/data/local/CacheDB.kt index 3e5402f7..37e9042a 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/data/local/CacheDB.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/data/local/CacheDB.kt @@ -2,6 +2,7 @@ package by.alexandr7035.gitstat.data.local import androidx.room.Database import androidx.room.RoomDatabase +import androidx.room.TypeConverters import by.alexandr7035.gitstat.data.local.dao.ContributionsDao import by.alexandr7035.gitstat.data.local.dao.RepositoriesDao import by.alexandr7035.gitstat.data.local.dao.UserDao @@ -13,8 +14,9 @@ import by.alexandr7035.gitstat.data.local.model.* ContributionRateEntity::class, ContributionsYearEntity::class, ContributionTypesEntity::class, - ContributionsMonthEntity::class], version = 20) + ContributionsMonthEntity::class], version = 21) +@TypeConverters(RoomTypeConverters::class) abstract class CacheDB : RoomDatabase() { abstract fun getUserDao(): UserDao diff --git a/app/src/main/java/by/alexandr7035/gitstat/data/local/RoomTypeConverters.kt b/app/src/main/java/by/alexandr7035/gitstat/data/local/RoomTypeConverters.kt new file mode 100644 index 00000000..217fe15e --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/data/local/RoomTypeConverters.kt @@ -0,0 +1,34 @@ +package by.alexandr7035.gitstat.data.local + +import androidx.room.ProvidedTypeConverter +import androidx.room.TypeConverter +import by.alexandr7035.gitstat.core.Language +import by.alexandr7035.gitstat.data.local.model.RepoLanguage +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +@ProvidedTypeConverter +class RoomTypeConverters(private val gson: Gson) { + + @TypeConverter + fun fromString(value: String?): ArrayList? { + val listType = object : TypeToken?>() {}.type + return gson.fromJson(value, listType) + } + + @TypeConverter + fun fromArrayList(list: ArrayList?): String? { + return gson.toJson(list) + } + + @TypeConverter + fun fromLanguage(languages: List?): String? { + return gson.toJson(languages) + } + + @TypeConverter + fun getLanguageFromString(languagesStr: String?): List? { + val listType = object : TypeToken?>() {}.type + return gson.fromJson(languagesStr, listType) + } +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/data/local/dao/RepositoriesDao.kt b/app/src/main/java/by/alexandr7035/gitstat/data/local/dao/RepositoriesDao.kt index ef462826..8d4b5fc6 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/data/local/dao/RepositoriesDao.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/data/local/dao/RepositoriesDao.kt @@ -15,6 +15,9 @@ interface RepositoriesDao { @Query("select * from repositories") fun getRepositoriesLiveData(): LiveData> + @Query("select * from repositories where id = (:repositoryId)") + fun getRepositoryLiveData(repositoryId: Int): LiveData + @Query("select * from repositories") suspend fun getRepositories(): List diff --git a/app/src/main/java/by/alexandr7035/gitstat/data/local/model/RepoLanguage.kt b/app/src/main/java/by/alexandr7035/gitstat/data/local/model/RepoLanguage.kt new file mode 100644 index 00000000..ef428cff --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/data/local/model/RepoLanguage.kt @@ -0,0 +1,7 @@ +package by.alexandr7035.gitstat.data.local.model + +data class RepoLanguage( + val name: String, + val color: String, + val size: Int +) \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/data/local/model/RepositoryEntity.kt b/app/src/main/java/by/alexandr7035/gitstat/data/local/model/RepositoryEntity.kt index bd074cc7..ac378197 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/data/local/model/RepositoryEntity.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/data/local/model/RepositoryEntity.kt @@ -2,20 +2,34 @@ package by.alexandr7035.gitstat.data.local.model import androidx.room.Entity import androidx.room.PrimaryKey +import by.alexandr7035.gitstat.core.Language @Entity(tableName = "repositories") class RepositoryEntity( -@PrimaryKey + @PrimaryKey var id: Int, -var name: String, -var language: String, -var languageColor: String, + var name: String, + var nameWithOwner: String, + var parentNameWithOwner: String, -var isPrivate: Boolean, -var isArchived: Boolean, -var isFork: Boolean, + var description: String, + var websiteUrl: String, -var stars: Int, + var primaryLanguage: String, + var primaryLanguageColor: String, -var created_at: Long) \ No newline at end of file + var languages: List, + + var isPrivate: Boolean, + var isArchived: Boolean, + var isFork: Boolean, + + var stars: Int, + + var created_at: Long, + var updated_at: Long, + + var diskUsageKB: Int, + + val topics: ArrayList) \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/data/remote/mappers/RepositoriesRemoteToCacheMapper.kt b/app/src/main/java/by/alexandr7035/gitstat/data/remote/mappers/RepositoriesRemoteToCacheMapper.kt index cb0b487a..44f025d7 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/data/remote/mappers/RepositoriesRemoteToCacheMapper.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/data/remote/mappers/RepositoriesRemoteToCacheMapper.kt @@ -3,7 +3,10 @@ package by.alexandr7035.gitstat.data.remote.mappers import by.alexandr7035.gitstat.apollo.RepositoriesQuery import by.alexandr7035.gitstat.core.Mapper import by.alexandr7035.gitstat.core.TimeHelper +import by.alexandr7035.gitstat.data.local.model.RepoLanguage import by.alexandr7035.gitstat.data.local.model.RepositoryEntity +import by.alexandr7035.gitstat.extensions.debug +import timber.log.Timber import javax.inject.Inject class RepositoriesRemoteToCacheMapper @Inject constructor(private val timeHelper: TimeHelper): Mapper> { @@ -35,11 +38,62 @@ class RepositoriesRemoteToCacheMapper @Inject constructor(private val timeHelper } } + val websiteUrl = if (repo.homepageUrl != null) { + repo.homepageUrl as String + } else { + "" + } + + val topics = ArrayList() + + if (repo.repositoryTopics.nodes != null) { + for (node in repo.repositoryTopics.nodes) { + if (node != null) { + topics.add(node.topic.name) + } + } + } + + Timber.debug("topics $topics") + + val languages = ArrayList() + + if (repo.languages?.edges != null) { + + if (repo.languages.edges.isNotEmpty()) { + + for (edge in repo.languages.edges) { + + val nodeLanguage = when (edge?.node?.name) { + null -> "Unknown" + else -> edge.node.name + } + + val nodeLanguageColor = when (edge?.node?.color) { + null -> "#C3C3C3" + else -> edge.node.color + } + + val size = edge?.size ?: 0 + + languages.add(RepoLanguage(name = nodeLanguage, color = nodeLanguageColor, size = size)) + } + } + else { + languages.add(RepoLanguage("Unknown", "#C3C3C3", size = 1)) + } + } + val repository = RepositoryEntity( id = repo.databaseId!!, name = repo.name, - language = language, - languageColor = languageColor, + nameWithOwner = repo.nameWithOwner, + parentNameWithOwner = repo.parent?.nameWithOwner ?: "", + + description = repo.description ?: "No repository description provided.", + websiteUrl = websiteUrl, + primaryLanguage = language, + primaryLanguageColor = languageColor, isPrivate = repo.isPrivate, isArchived = repo.isArchived, @@ -47,7 +101,13 @@ class RepositoriesRemoteToCacheMapper @Inject constructor(private val timeHelper stars = repo.stargazerCount, - created_at = timeHelper.getUnixDateFromISO8601(repo.createdAt as String) + created_at = timeHelper.getUnixDateFromISO8601(repo.createdAt as String), + updated_at = timeHelper.getUnixDateFromISO8601(repo.updatedAt as String), + + diskUsageKB = repo.diskUsage ?: 0, + + topics = topics, + languages = languages ) cachedList.add(repository) diff --git a/app/src/main/java/by/alexandr7035/gitstat/di/AppModule.kt b/app/src/main/java/by/alexandr7035/gitstat/di/AppModule.kt index b2445945..c9267295 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/di/AppModule.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/di/AppModule.kt @@ -6,6 +6,7 @@ import by.alexandr7035.gitstat.core.KeyValueStorage import by.alexandr7035.gitstat.core.TimeHelper import by.alexandr7035.gitstat.data.* import by.alexandr7035.gitstat.data.local.CacheDB +import by.alexandr7035.gitstat.data.local.RoomTypeConverters import by.alexandr7035.gitstat.data.local.dao.ContributionsDao import by.alexandr7035.gitstat.data.local.dao.RepositoriesDao import by.alexandr7035.gitstat.data.local.dao.UserDao @@ -133,9 +134,10 @@ object AppModule { @Provides @Singleton - fun provideRoomDb(application: Application): CacheDB { + fun provideRoomDb(application: Application, gson: Gson): CacheDB { return Room .databaseBuilder(application.applicationContext, CacheDB::class.java, "cache.db") + .addTypeConverter(RoomTypeConverters(gson)) .fallbackToDestructiveMigration() .build() } diff --git a/app/src/main/java/by/alexandr7035/gitstat/extensions/BarChart.kt b/app/src/main/java/by/alexandr7035/gitstat/extensions/BarChart.kt deleted file mode 100644 index 21598bb5..00000000 --- a/app/src/main/java/by/alexandr7035/gitstat/extensions/BarChart.kt +++ /dev/null @@ -1,46 +0,0 @@ -package by.alexandr7035.gitstat.extensions - -import android.graphics.Color -import android.graphics.Typeface -import com.github.mikephil.charting.charts.BarChart -import com.github.mikephil.charting.data.BarData -import com.github.mikephil.charting.data.BarDataSet -import com.github.mikephil.charting.formatter.ValueFormatter - -fun BarChart.setupHorizontalBarChart(valueFormatter: ValueFormatter?) { - // Disable legend and description - // Use custom legend based on RecyclerView - description.isEnabled = false - legend.isEnabled = false - - // Disable scaling - setScaleEnabled(false) - - // Disable xAxis and axisRight - // NOTE: as plot is rotated to HORIZONTAL, - // axisLeft is the primary axis (normally it would be an xAxis) - xAxis.isEnabled = false - axisRight.isEnabled = false - - // Setup left axis - axisLeft.textSize = 16f - axisLeft.setDrawAxisLine(false) - axisLeft.setDrawGridLines(false) - axisLeft.valueFormatter = valueFormatter - // Space between axis and labels - axisLeft.yOffset = 10f -} - - -fun BarChart.setChartData(dataset: BarDataSet) { - - dataset.apply { - setTouchEnabled(false) - valueTextSize = 20f - valueTextColor = Color.WHITE - valueTypeface = Typeface.defaultFromStyle(Typeface.BOLD) - } - - data = BarData(dataset) - data.setDrawValues(false) -} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/extensions/Long.kt b/app/src/main/java/by/alexandr7035/gitstat/extensions/Long.kt new file mode 100644 index 00000000..bb497f13 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/extensions/Long.kt @@ -0,0 +1,18 @@ +package by.alexandr7035.gitstat.extensions + +import java.text.SimpleDateFormat +import java.util.* + +fun Long.getStringDateFromLong( + stringFormat: String, + timezoneStr: String? = null, + locale: Locale = Locale.US +): String { + val format = SimpleDateFormat(stringFormat, locale) + + if (timezoneStr != null) { + format.timeZone = TimeZone.getTimeZone(timezoneStr) + } + + return format.format(this) +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/extensions/PieChart.kt b/app/src/main/java/by/alexandr7035/gitstat/extensions/PieChart.kt new file mode 100644 index 00000000..17706ce7 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/extensions/PieChart.kt @@ -0,0 +1,44 @@ +package by.alexandr7035.gitstat.extensions + +import android.graphics.Color +import android.graphics.Typeface +import by.alexandr7035.gitstat.view.repositories.plots.languages_plot.PieDataValueFormatter +import com.github.mikephil.charting.charts.PieChart +import com.github.mikephil.charting.components.Legend +import com.github.mikephil.charting.data.PieData +import com.github.mikephil.charting.data.PieDataSet +import com.github.mikephil.charting.formatter.ValueFormatter + +fun PieChart.setupPieChartView(isLegendEnabled: Boolean = true) { + setEntryLabelTextSize(16f) + setDrawEntryLabels(false) + description.isEnabled = false + setCenterTextSize(30f) + + setExtraOffsets(0f, 0f, 0f, 0f) + + if (isLegendEnabled) { + legend.verticalAlignment = Legend.LegendVerticalAlignment.BOTTOM + legend.horizontalAlignment = Legend.LegendHorizontalAlignment.LEFT + legend.orientation = Legend.LegendOrientation.HORIZONTAL + legend.setDrawInside(false) + legend.isWordWrapEnabled = true + legend.textSize = 20f + legend.xEntrySpace = 20f + } + else { + legend.isEnabled = false + } + +} + +fun PieChart.setPieChartData(dataset: PieDataSet, valueFormatter: ValueFormatter) { + dataset.valueTextSize = 20f + dataset.valueTextColor = Color.BLACK + dataset.valueTypeface = Typeface.defaultFromStyle(Typeface.BOLD) + + val pieData = PieData(dataset) + pieData.setValueFormatter(valueFormatter) + + data = pieData +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/extensions/YAxis.kt b/app/src/main/java/by/alexandr7035/gitstat/extensions/YAxis.kt index f3ba6f9b..ca580c88 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/extensions/YAxis.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/extensions/YAxis.kt @@ -29,16 +29,4 @@ fun YAxis.setupYAxisValuesForContributionRate(topValue: Float) { } -fun YAxis.setupYAxisValuesForContributionTypes(topValue: Int) { - - axisMaximum = if (topValue % 100 == 0) { - topValue.toFloat() - } else { - (floor(topValue / 100F) + 1) * 100 - } - - axisMinimum = 0f - - setLabelCount(5, true) -} diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/MainActivity.kt b/app/src/main/java/by/alexandr7035/gitstat/view/MainActivity.kt index b78f4c29..beeff613 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/MainActivity.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController +import by.alexandr7035.gitstat.BuildConfig import by.alexandr7035.gitstat.NavGraphDirections import by.alexandr7035.gitstat.R import by.alexandr7035.gitstat.data.SyncForegroundService @@ -127,12 +128,17 @@ class MainActivity : AppCompatActivity() { binding.drawerLayout.closeDrawer(GravityCompat.START) } - R.id.item_privacy_policy -> navController.navigateSafe(NavGraphDirections.actionGlobalWebViewFragment( + R.id.item_privacy_policy -> navController.navigateSafe(NavGraphDirections.actionGlobalInfoFragment( getString(R.string.privacy_policy), - getString(R.string.privacy_policy_url) + null, + getString(R.string.privacy_policy_full_text) )) - R.id.item_about_app -> navController.navigateSafe(NavGraphDirections.actionGlobalAboutAppFragment()) + R.id.item_about_app -> navController.navigateSafe(NavGraphDirections.actionGlobalInfoFragment( + getString(R.string.about_app), + getString(R.string.app_name_with_version, BuildConfig.VERSION_NAME), + getString(R.string.app_description) + )) } true diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/ContributionsFragment.kt b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/ContributionsFragment.kt index 59c8f94b..87ad12cd 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/ContributionsFragment.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/ContributionsFragment.kt @@ -7,21 +7,16 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.ViewPager2 import by.alexandr7035.gitstat.R import by.alexandr7035.gitstat.databinding.FragmentContributionsBinding import by.alexandr7035.gitstat.extensions.navigateSafe -import by.alexandr7035.gitstat.extensions.setChartData -import by.alexandr7035.gitstat.extensions.setupHorizontalBarChart -import by.alexandr7035.gitstat.extensions.setupYAxisValuesForContributionTypes import by.alexandr7035.gitstat.view.MainActivity import by.alexandr7035.gitstat.view.contributions.plots.contributions_per_year.YearContributionsAdapter import by.alexandr7035.gitstat.view.contributions.plots.contributions_rate.YearContributionRatesAdapter -import by.alexandr7035.gitstat.view.contributions.plots.contributions_types.RemoveThousandsSepFormatter -import by.alexandr7035.gitstat.view.contributions.plots.contributions_types.TypesLegendAdapter -import by.alexandr7035.gitstat.view.contributions.plots.contributions_types.model.ContributionTypesListToBarDataSetMapper -import by.alexandr7035.gitstat.view.contributions.plots.contributions_types.model.ContributionTypesListToLegendItemsMapper -import com.google.android.flexbox.FlexboxLayoutManager +import by.alexandr7035.gitstat.view.contributions.plots.contributions_types.TypesAdapter +import by.alexandr7035.gitstat.view.contributions.plots.contributions_types.model.ContributionTypesListToRecyclerItemsMapper import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber @@ -49,9 +44,9 @@ class ContributionsFragment : Fragment() { binding?.rateViewPager?.adapter = yearContributionsRateAdapter // Adapter for legend on contribution types plot - val typesLegendAdapter = TypesLegendAdapter() - binding?.contributionTypesLegendRecycler?.layoutManager = FlexboxLayoutManager(requireContext()) - binding?.contributionTypesLegendRecycler?.adapter = typesLegendAdapter + val typesLegendAdapter = TypesAdapter() + binding?.contributionTypesRecycler?.layoutManager = LinearLayoutManager(requireContext()) + binding?.contributionTypesRecycler?.adapter = typesLegendAdapter // Update data viewModel.getContributionYearsLiveData().observe(viewLifecycleOwner, { years -> @@ -118,42 +113,10 @@ class ContributionsFragment : Fragment() { viewModel.getContributionTypesLiveData().observe(viewLifecycleOwner, { typesData -> if (typesData != null) { - - // Detect max value - // FIXME find better solution - val commits = typesData.sumOf { it.commitContributions } - val issues = typesData.sumOf { it.issueContributions } - val pullRequests = typesData.sumOf { it.pullRequestContributions } - val reviews = typesData.sumOf { it.pullRequestReviewContributions } - val repositories = typesData.sumOf { it.repositoryContributions } - val unknown = typesData.sumOf { it.unknownContributions } - - // FIXME refactoring - val maxValue = listOf( - commits, - issues, - pullRequests, - reviews, - repositories, - unknown - ).maxByOrNull { - it - } ?: 0 - - - // Setup chart - binding?.contributionTypesChart?.setupHorizontalBarChart(RemoveThousandsSepFormatter()) - binding?.contributionTypesChart?.setExtraOffsets(10f,0f,30f,0f) - - // Chart axis - binding?.contributionTypesChart?.axisLeft?.setupYAxisValuesForContributionTypes(maxValue) - - // Populate chart with data - binding?.contributionTypesChart?.setChartData(ContributionTypesListToBarDataSetMapper.map(typesData, requireContext())) - binding?.contributionTypesChart?.invalidate() - // Update legend - typesLegendAdapter.setItems(ContributionTypesListToLegendItemsMapper.map(typesData, requireContext())) + binding?.contributionTypesRecycler?.suppressLayout(false) + typesLegendAdapter.setItems(ContributionTypesListToRecyclerItemsMapper.map(typesData, requireContext())) + binding?.contributionTypesRecycler?.suppressLayout(true) } }) @@ -181,6 +144,10 @@ class ContributionsFragment : Fragment() { binding?.toContributionsGridBtn?.setOnClickListener { findNavController().navigateSafe(ContributionsFragmentDirections.actionContributionsFragmentToContributionsGridFragment(2021)) } + + binding?.contributionTypesCard?.setOnClickListener { + findNavController().navigateSafe(ContributionsFragmentDirections.actionContributionsFragmentToFragmentContributionTypes()) + } } } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/FragmentContributionTypes.kt b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/FragmentContributionTypes.kt new file mode 100644 index 00000000..ea51121a --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/FragmentContributionTypes.kt @@ -0,0 +1,96 @@ +package by.alexandr7035.gitstat.view.contributions.plots.contributions_types + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import by.alexandr7035.gitstat.R +import by.alexandr7035.gitstat.databinding.FragmentContributionTypesBinding +import by.alexandr7035.gitstat.extensions.setPieChartData +import by.alexandr7035.gitstat.extensions.setupPieChartView +import by.alexandr7035.gitstat.view.contributions.ContributionsViewModel +import by.alexandr7035.gitstat.view.contributions.plots.contributions_types.model.ContributionTypesListToRecyclerItemsMapper +import by.alexandr7035.gitstat.view.repositories.plots.languages_plot.PieDataValueFormatter +import com.github.mikephil.charting.data.PieDataSet +import com.github.mikephil.charting.data.PieEntry +import dagger.hilt.android.AndroidEntryPoint + + +@AndroidEntryPoint +class FragmentContributionTypes : Fragment() { + + private var binding: FragmentContributionTypesBinding? = null + private val viewModel by viewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + binding = FragmentContributionTypesBinding.inflate(inflater, container, false) + return binding?.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding?.toolbar?.setNavigationOnClickListener { + findNavController().navigateUp() + } + + val adapter = TypesAdapter(showCount = true) + binding?.typesRecycler?.layoutManager = LinearLayoutManager(requireContext()) + binding?.typesRecycler?.adapter = adapter + + viewModel.getContributionTypesLiveData().observe(viewLifecycleOwner, { typesData -> + // Detect max value + // FIXME find better solution + val commits = typesData.sumOf { it.commitContributions } + val issues = typesData.sumOf { it.issueContributions } + val pullRequests = typesData.sumOf { it.pullRequestContributions } + val reviews = typesData.sumOf { it.pullRequestReviewContributions } + val repositories = typesData.sumOf { it.repositoryContributions } + val unknown = typesData.sumOf { it.unknownContributions } + + val total = typesData.sumOf { it.totalContributions } + + val entries = listOf( + PieEntry(commits.toFloat(), "Commits"), + PieEntry(issues.toFloat(), "Issues"), + PieEntry(pullRequests.toFloat(), "Pull requests"), + PieEntry(reviews.toFloat(), "Code reviews"), + PieEntry(repositories.toFloat(), "Repositories"), + PieEntry(unknown.toFloat(), "Unknown") + ) + + val dataSet = PieDataSet(entries, "") + + val colors = listOf( + ContextCompat.getColor(requireContext(), R.color.color_commits), + ContextCompat.getColor(requireContext(), R.color.color_issues), + ContextCompat.getColor(requireContext(), R.color.color_pull_requests), + ContextCompat.getColor(requireContext(), R.color.color_code_reviews), + ContextCompat.getColor(requireContext(), R.color.color_repositories), + ContextCompat.getColor(requireContext(), R.color.color_unknown_contributions), + ) + + dataSet.colors = colors + dataSet.setDrawValues(false) + + binding?.typesChart?.setupPieChartView(isLegendEnabled = false) + binding?.typesChart?.setPieChartData(dataSet, PieDataValueFormatter()) + binding?.typesChart?.centerText = total.toString() + + binding?.typesChart?.invalidate() + + adapter.setItems(ContributionTypesListToRecyclerItemsMapper.map(typesData, requireContext())) + }) + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/RemoveThousandsSepFormatter.kt b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/RemoveThousandsSepFormatter.kt deleted file mode 100644 index 39d9cc97..00000000 --- a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/RemoveThousandsSepFormatter.kt +++ /dev/null @@ -1,11 +0,0 @@ -package by.alexandr7035.gitstat.view.contributions.plots.contributions_types - -import com.github.mikephil.charting.formatter.ValueFormatter - -// Simple solution to remove space like "1 000" in thousands -// Do not use with Floats!de -class RemoveThousandsSepFormatter : ValueFormatter() { - override fun getFormattedValue(value: Float): String { - return value.toInt().toString() - } -} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/TypesLegendAdapter.kt b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/TypesAdapter.kt similarity index 56% rename from app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/TypesLegendAdapter.kt rename to app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/TypesAdapter.kt index 78af246f..cd89dabb 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/TypesLegendAdapter.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/TypesAdapter.kt @@ -3,25 +3,31 @@ package by.alexandr7035.gitstat.view.contributions.plots.contributions_types import android.annotation.SuppressLint import android.graphics.drawable.GradientDrawable import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import by.alexandr7035.gitstat.databinding.ViewContributionTypesPlotLegendItemBinding -import by.alexandr7035.gitstat.view.contributions.plots.contributions_types.model.TypesLegendItem +import by.alexandr7035.gitstat.databinding.ViewContributionTypeItemBinding +import by.alexandr7035.gitstat.view.contributions.plots.contributions_types.model.TypesItem -class TypesLegendAdapter: RecyclerView.Adapter() { +class TypesAdapter(private val showCount: Boolean = false): RecyclerView.Adapter() { - private var items: List = emptyList() + private var items: List = emptyList() - class ViewHolder(val binding: ViewContributionTypesPlotLegendItemBinding): RecyclerView.ViewHolder(binding.root) + class ViewHolder(val binding: ViewContributionTypeItemBinding): RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val binding = ViewContributionTypesPlotLegendItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = ViewContributionTypeItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - // FIXME test - holder.binding.countView.text = items[position].count.toString() + + if (showCount) { + holder.binding.countView.text = items[position].count.toString() + } + else { + holder.binding.countView.visibility = View.GONE + } holder.binding.label.text = items[position].label holder.binding.percentageView.text = items[position].percentage @@ -33,7 +39,7 @@ class TypesLegendAdapter: RecyclerView.Adapter() } @SuppressLint("NotifyDataSetChanged") - fun setItems(items: List) { + fun setItems(items: List) { this.items = items notifyDataSetChanged() } diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/ContributionTypesListToBarDataSetMapper.kt b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/ContributionTypesListToBarDataSetMapper.kt deleted file mode 100644 index 6e1fc76e..00000000 --- a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/ContributionTypesListToBarDataSetMapper.kt +++ /dev/null @@ -1,48 +0,0 @@ -package by.alexandr7035.gitstat.view.contributions.plots.contributions_types.model - -import android.content.Context -import androidx.core.content.ContextCompat -import by.alexandr7035.gitstat.R -import by.alexandr7035.gitstat.data.local.model.ContributionTypesEntity -import com.github.mikephil.charting.data.BarDataSet -import com.github.mikephil.charting.data.BarEntry - -class ContributionTypesListToBarDataSetMapper private constructor() { - - companion object { - - fun map(data: List, context: Context): BarDataSet { - - // Count types for all of the years - val commits = data.sumOf { it.commitContributions } - val issues = data.sumOf { it.issueContributions } - val pullRequests = data.sumOf { it.pullRequestContributions } - val reviews = data.sumOf { it.pullRequestReviewContributions } - val repositories = data.sumOf { it.repositoryContributions } - val unknown = data.sumOf { it.unknownContributions } - - // Change default order with negative values - val entries = ArrayList() - entries.add(BarEntry(0f, commits.toFloat())) - entries.add(BarEntry(-1f, repositories.toFloat())) - entries.add(BarEntry(-2f, issues.toFloat())) - entries.add(BarEntry(-3f, pullRequests.toFloat())) - entries.add(BarEntry(-4f, reviews.toFloat())) - entries.add(BarEntry(-5f, unknown.toFloat())) - - val colors = listOf( - ContextCompat.getColor(context, R.color.color_commits), - ContextCompat.getColor(context, R.color.color_repositories), - ContextCompat.getColor(context, R.color.color_issues), - ContextCompat.getColor(context, R.color.color_pull_requests), - ContextCompat.getColor(context, R.color.color_code_reviews), - ContextCompat.getColor(context, R.color.color_unknown_contributions) - ) - - val dataSet = BarDataSet(entries, "") - dataSet.colors = colors - - return dataSet - } - } -} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/ContributionTypesListToLegendItemsMapper.kt b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/ContributionTypesListToRecyclerItemsMapper.kt similarity index 90% rename from app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/ContributionTypesListToLegendItemsMapper.kt rename to app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/ContributionTypesListToRecyclerItemsMapper.kt index b8b4e9cb..e9803d9c 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/ContributionTypesListToLegendItemsMapper.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/ContributionTypesListToRecyclerItemsMapper.kt @@ -7,11 +7,11 @@ import by.alexandr7035.gitstat.data.local.model.ContributionTypesEntity import kotlin.math.ceil // FIXME architecture -class ContributionTypesListToLegendItemsMapper private constructor() { +class ContributionTypesListToRecyclerItemsMapper private constructor() { companion object { - fun map(data: List, context: Context): List { + fun map(data: List, context: Context): List { // Count types for all of the years val commits = data.sumOf { it.commitContributions } @@ -23,38 +23,38 @@ class ContributionTypesListToLegendItemsMapper private constructor() { val total = data.sumOf { it.totalContributions } return listOf( - TypesLegendItem( + TypesItem( label = "Commits", count = commits, percentage = getPercentage(commits / total.toFloat() * 100), color = ContextCompat.getColor(context, R.color.color_commits) ), - TypesLegendItem( + TypesItem( label = "Repositories", count = repositories, percentage = getPercentage(repositories / total.toFloat() * 100), color = ContextCompat.getColor(context, R.color.color_repositories) ), - TypesLegendItem( + TypesItem( label = "Issues", count = issues, percentage = getPercentage(issues / total.toFloat() * 100), color = ContextCompat.getColor(context, R.color.color_issues) ), - TypesLegendItem( + TypesItem( label = "Pull requests", count = pullRequests, percentage = getPercentage(pullRequests / total.toFloat() * 100), color = ContextCompat.getColor(context, R.color.color_pull_requests) ), - TypesLegendItem( + TypesItem( label = "Code reviews", count = reviews, percentage = getPercentage(reviews / total.toFloat() * 100), color = ContextCompat.getColor(context, R.color.color_code_reviews) ), - TypesLegendItem( + TypesItem( label = "Unknown", count = unknown, percentage = getPercentage(unknown / total.toFloat() * 100), diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/TypesLegendItem.kt b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/TypesItem.kt similarity index 88% rename from app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/TypesLegendItem.kt rename to app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/TypesItem.kt index 84c0826e..f223af30 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/TypesLegendItem.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/contributions/plots/contributions_types/model/TypesItem.kt @@ -1,6 +1,6 @@ package by.alexandr7035.gitstat.view.contributions.plots.contributions_types.model -class TypesLegendItem( +class TypesItem( val count: Int, val percentage: String, val label: String, diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/core/info_dialog/InfoDialogFragment.kt b/app/src/main/java/by/alexandr7035/gitstat/view/info/InfoDialogFragment.kt similarity index 96% rename from app/src/main/java/by/alexandr7035/gitstat/view/core/info_dialog/InfoDialogFragment.kt rename to app/src/main/java/by/alexandr7035/gitstat/view/info/InfoDialogFragment.kt index 86f470b3..da48fc52 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/core/info_dialog/InfoDialogFragment.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/info/InfoDialogFragment.kt @@ -1,4 +1,4 @@ -package by.alexandr7035.gitstat.view.core.info_dialog +package by.alexandr7035.gitstat.view.info import android.os.Bundle import android.view.LayoutInflater diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/info/AboutAppFragment.kt b/app/src/main/java/by/alexandr7035/gitstat/view/info/InfoFragment.kt similarity index 55% rename from app/src/main/java/by/alexandr7035/gitstat/view/info/AboutAppFragment.kt rename to app/src/main/java/by/alexandr7035/gitstat/view/info/InfoFragment.kt index 15dab667..d25ae2f9 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/info/AboutAppFragment.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/info/InfoFragment.kt @@ -7,17 +7,17 @@ import android.view.ViewGroup import androidx.core.text.HtmlCompat import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import by.alexandr7035.gitstat.BuildConfig -import by.alexandr7035.gitstat.R -import by.alexandr7035.gitstat.databinding.FragmentAboutAppBinding +import androidx.navigation.fragment.navArgs +import by.alexandr7035.gitstat.databinding.FragmentInfoBinding -class AboutAppFragment : Fragment() { +class InfoFragment : Fragment() { - private var binding: FragmentAboutAppBinding? = null + private var binding: FragmentInfoBinding? = null + private val safeArgs by navArgs() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // Inflate the layout for this fragment - binding = FragmentAboutAppBinding.inflate(inflater, container, false) + binding = FragmentInfoBinding.inflate(inflater, container, false) return binding?.root } @@ -28,8 +28,14 @@ class AboutAppFragment : Fragment() { findNavController().navigateUp() } - binding?.appMame?.text = getString(R.string.app_name_with_version, BuildConfig.VERSION_NAME) - binding?.descriptionText?.text = HtmlCompat.fromHtml(getString(R.string.app_description), HtmlCompat.FROM_HTML_MODE_LEGACY) + binding?.toolbar?.title = safeArgs.toolbarTitle + if (safeArgs.infoTitle != null) { + binding?.infoTitle?.text = safeArgs.infoTitle + } + else { + binding?.infoTitle?.visibility = View.GONE + } + binding?.infoText?.text = HtmlCompat.fromHtml(safeArgs.infoText, HtmlCompat.FROM_HTML_MODE_LEGACY) } override fun onDestroyView() { diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/login/LoginFragment.kt b/app/src/main/java/by/alexandr7035/gitstat/view/login/LoginFragment.kt index 7c8f2207..766c3a65 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/login/LoginFragment.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/login/LoginFragment.kt @@ -13,6 +13,7 @@ import androidx.fragment.app.viewModels import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import by.alexandr7035.gitstat.BuildConfig +import by.alexandr7035.gitstat.NavGraphDirections import by.alexandr7035.gitstat.R import by.alexandr7035.gitstat.core.GithubAccessScopes import by.alexandr7035.gitstat.databinding.FragmentLoginBinding @@ -102,10 +103,10 @@ class LoginFragment: Fragment() { } private fun showPrivacyPolicy() { - navController.navigateSafe( - LoginFragmentDirections.actionLoginFragmentToWebViewFragment( + navController.navigateSafe(NavGraphDirections.actionGlobalInfoFragment( getString(R.string.privacy_policy), - getString(R.string.privacy_policy_url) + null, + getString(R.string.privacy_policy_full_text) )) } diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/ActiveRepositoriesFragment.kt b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/ActiveRepositoriesFragment.kt index 97fe9894..928fc503 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/ActiveRepositoriesFragment.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/ActiveRepositoriesFragment.kt @@ -5,15 +5,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController import androidx.navigation.navGraphViewModels import androidx.recyclerview.widget.LinearLayoutManager import by.alexandr7035.gitstat.R +import by.alexandr7035.gitstat.RepositoriesListGraphDirections import by.alexandr7035.gitstat.databinding.FragmentRepositoriesRecyclerBinding +import by.alexandr7035.gitstat.extensions.navigateSafe import by.alexandr7035.gitstat.view.repositories.filters.RepositoriesListFiltersHelper import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class ActiveRepositoriesFragment : Fragment() { +class ActiveRepositoriesFragment : Fragment(), RepoClickListener { private val viewModel by navGraphViewModels(R.id.repositoriesListGraph) { defaultViewModelProviderFactory } private var binding: FragmentRepositoriesRecyclerBinding? = null @@ -29,7 +32,7 @@ class ActiveRepositoriesFragment : Fragment() { super.onViewCreated(view, savedInstanceState) // Setup adapter - adapter = RepositoriesAdapter() + adapter = RepositoriesAdapter(this) binding!!.recycler.adapter = adapter binding!!.recycler.layoutManager = LinearLayoutManager(context) @@ -76,4 +79,8 @@ class ActiveRepositoriesFragment : Fragment() { super.onDestroyView() binding = null } + + override fun onRepoClicked(repoId: Int) { + findNavController().navigateSafe(RepositoriesListGraphDirections.actionGlobalRepositoryPageFragment(repoId)) + } } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/ArchivedRepositoriesFragment.kt b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/ArchivedRepositoriesFragment.kt index abe1b80f..133395d0 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/ArchivedRepositoriesFragment.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/ArchivedRepositoriesFragment.kt @@ -5,15 +5,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController import androidx.navigation.navGraphViewModels import androidx.recyclerview.widget.LinearLayoutManager import by.alexandr7035.gitstat.R +import by.alexandr7035.gitstat.RepositoriesListGraphDirections import by.alexandr7035.gitstat.databinding.FragmentRepositoriesRecyclerBinding +import by.alexandr7035.gitstat.extensions.navigateSafe import by.alexandr7035.gitstat.view.repositories.filters.RepositoriesListFiltersHelper import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class ArchivedRepositoriesFragment : Fragment() { +class ArchivedRepositoriesFragment : Fragment(), RepoClickListener { private val viewModel by navGraphViewModels(R.id.repositoriesListGraph) { defaultViewModelProviderFactory } private var binding: FragmentRepositoriesRecyclerBinding? = null @@ -30,7 +33,7 @@ class ArchivedRepositoriesFragment : Fragment() { super.onViewCreated(view, savedInstanceState) // Setup adapter - adapter = RepositoriesAdapter() + adapter = RepositoriesAdapter(this) binding!!.recycler.adapter = adapter binding!!.recycler.layoutManager = LinearLayoutManager(context) @@ -78,4 +81,8 @@ class ArchivedRepositoriesFragment : Fragment() { binding = null } + override fun onRepoClicked(repoId: Int) { + findNavController().navigateSafe(RepositoriesListGraphDirections.actionGlobalRepositoryPageFragment(repoId)) + } + } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepoClickListener.kt b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepoClickListener.kt new file mode 100644 index 00000000..a3e096cc --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepoClickListener.kt @@ -0,0 +1,5 @@ +package by.alexandr7035.gitstat.view.repositories + +interface RepoClickListener { + fun onRepoClicked(repoId: Int) +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepoLanguagesAdapter.kt b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepoLanguagesAdapter.kt new file mode 100644 index 00000000..9637c754 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepoLanguagesAdapter.kt @@ -0,0 +1,43 @@ +package by.alexandr7035.gitstat.view.repositories + +import android.annotation.SuppressLint +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import by.alexandr7035.gitstat.data.local.model.RepoLanguage +import by.alexandr7035.gitstat.databinding.ViewRepositoryLanguageBinding + +class RepoLanguagesAdapter: RecyclerView.Adapter() { + + private var items: List = emptyList() + private var totalLanguagesSize = 0 + + override fun getItemCount(): Int { + return items.size + } + + @SuppressLint("NotifyDataSetChanged") + fun setItems(items: List) { + this.items = items + totalLanguagesSize = items.sumOf { it.size } + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ViewRepositoryLanguageBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.binding.languageNameView.text = items[position].name + (holder.binding.languageColorView.background as GradientDrawable?)?.setColor(Color.parseColor(items[position].color)) + + val percentage = String.format("%.2f", items[position].size.toFloat() / totalLanguagesSize.toFloat() * 100) + holder.binding.languagePercentage.text = "${percentage}%" + } + + class ViewHolder(val binding: ViewRepositoryLanguageBinding): RecyclerView.ViewHolder(binding.root) + +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepoTopicsAdapter.kt b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepoTopicsAdapter.kt new file mode 100644 index 00000000..fae1416b --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepoTopicsAdapter.kt @@ -0,0 +1,34 @@ +package by.alexandr7035.gitstat.view.repositories + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import by.alexandr7035.gitstat.databinding.ViewRepositoryTopicBinding + +class RepoTopicsAdapter: RecyclerView.Adapter() { + + private var items: List = emptyList() + + @SuppressLint("NotifyDataSetChanged") + fun setItems(items: List) { + this.items = items + notifyDataSetChanged() + } + + override fun getItemCount(): Int { + return items.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ViewRepositoryTopicBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.binding.root.text = items[position] + } + + class ViewHolder(val binding: ViewRepositoryTopicBinding): RecyclerView.ViewHolder(binding.root) + +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoriesAdapter.kt b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoriesAdapter.kt index 915e7eae..98d2aad6 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoriesAdapter.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoriesAdapter.kt @@ -13,7 +13,7 @@ import by.alexandr7035.gitstat.R import by.alexandr7035.gitstat.data.local.model.RepositoryEntity import by.alexandr7035.gitstat.databinding.ViewRepositoryBinding -class RepositoriesAdapter : RecyclerView.Adapter() { +class RepositoriesAdapter(private val clickListener: RepoClickListener) : RecyclerView.Adapter() { private var items: List = ArrayList() private val createdDateFormat = "yyyy-MM-dd" @@ -34,7 +34,7 @@ class RepositoriesAdapter : RecyclerView.Adapter override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.binding.repoName.text = items[position].name holder.binding.createdDate.text = DateFormat.format(createdDateFormat, items[position].created_at) - holder.binding.language.text = items[position].language + holder.binding.language.text = items[position].primaryLanguage holder.binding.stars.text = items[position].stars.toString() when (items[position].isPrivate) { @@ -68,9 +68,18 @@ class RepositoriesAdapter : RecyclerView.Adapter false -> holder.binding.repoIsForkView.visibility = View.INVISIBLE } - (holder.binding.languageColorView.background as GradientDrawable).setColor(Color.parseColor(items[position].languageColor)) + (holder.binding.languageColorView.background as GradientDrawable).setColor(Color.parseColor(items[position].primaryLanguageColor)) } - class ViewHolder(val binding: ViewRepositoryBinding): RecyclerView.ViewHolder(binding.root) + inner class ViewHolder(val binding: ViewRepositoryBinding): RecyclerView.ViewHolder(binding.root), View.OnClickListener { + + init { + binding.root.setOnClickListener(this) + } + + override fun onClick(v: View) { + clickListener.onRepoClicked(items[adapterPosition].id) + } + } } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoriesViewModel.kt b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoriesViewModel.kt index 77bd4520..0cea2023 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoriesViewModel.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoriesViewModel.kt @@ -26,6 +26,10 @@ class RepositoriesViewModel @Inject constructor(private val repository: ReposRep return repository.getRepositoriesLiveData() } + fun getRepositoryLiveData(repositoryId: Int): LiveData { + return repository.getRepositoryLiveData(repositoryId) + } + // Repos livedata fun getAllRepositoriesListLiveData(): LiveData> { return allRepositoriesLiveData diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoryPageFragment.kt b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoryPageFragment.kt new file mode 100644 index 00000000..9017a4a7 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/RepositoryPageFragment.kt @@ -0,0 +1,171 @@ +package by.alexandr7035.gitstat.view.repositories + +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.net.Uri +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.text.HtmlCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import by.alexandr7035.gitstat.R +import by.alexandr7035.gitstat.databinding.FragmentRepositoryPageBinding +import by.alexandr7035.gitstat.extensions.debug +import by.alexandr7035.gitstat.extensions.getClickableSpannable +import by.alexandr7035.gitstat.extensions.getStringDateFromLong +import com.google.android.flexbox.FlexboxLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber + +@AndroidEntryPoint +class RepositoryPageFragment : Fragment() { + + private var binding: FragmentRepositoryPageBinding? = null + private val viewModel by viewModels() + private val safeArgs by navArgs() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + binding = FragmentRepositoryPageBinding.inflate(inflater, container, false) + return binding?.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding?.toolbar?.setNavigationOnClickListener { + findNavController().navigateUp() + } + + viewModel.getRepositoryLiveData(safeArgs.repositoryId).observe(viewLifecycleOwner, { repoData -> + + if (repoData != null) { + + Timber.debug("repo page ${repoData.description}") + + binding?.toolbar?.title = repoData.name + binding?.repoName?.text = repoData.nameWithOwner + binding?.language?.text = repoData.primaryLanguage + binding?.stars?.text = repoData.stars.toString() + + binding?.repoDescription?.text = repoData.description + + // Set parent (if fork) + if (repoData.isFork) { + binding?.parentName?.text = HtmlCompat.fromHtml( + getString(R.string.forked_from_template, repoData.parentNameWithOwner), + HtmlCompat.FROM_HTML_MODE_LEGACY + ) + } else { + binding?.parentName?.visibility = View.GONE + } + + // Set website link if exists + if (repoData.websiteUrl != "") { + + // Make link clickable + val urlText = repoData.websiteUrl + + val clickListener = View.OnClickListener { + Timber.debug("click link") + val webpage: Uri = Uri.parse(urlText) + val intent = Intent(Intent.ACTION_VIEW, webpage) + startActivity(intent) + } + + val spannableUrl = urlText.getClickableSpannable( + clickListener = clickListener, + clickableText = urlText, + isBold = false, + spannableColor = ContextCompat.getColor(requireContext(), R.color.blue_500) + ) + + binding?.websiteUrl?.text = spannableUrl + + binding?.websiteUrl?.apply { + movementMethod = LinkMovementMethod.getInstance() + highlightColor = Color.TRANSPARENT + } + + } else { + binding?.websiteUrl?.visibility = View.GONE + } + + // Change mark color depending on repo private/public + when (repoData.isPrivate) { + true -> { + binding?.repoVisibility?.text = getString(R.string.private_start_capital) + binding?.repoVisibility?.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.white + ) + ) + + binding?.repoVisibility?.background = ContextCompat.getDrawable( + requireContext(), + R.drawable.background_repo_visibilily_private + ) + } + else -> { + binding?.repoVisibility?.text = getString(R.string.public_start_capital) + binding?.repoVisibility?.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.black + ) + ) + binding?.repoVisibility?.background = ContextCompat.getDrawable( + requireContext(), + R.drawable.background_repo_visibilily + ) + } + } + + (binding?.languageColorView?.background as GradientDrawable?)?.setColor(Color.parseColor(repoData.primaryLanguageColor)) + + // Setup topics recycler + val topicsAdapter = RepoTopicsAdapter() + binding?.topicsRecycler?.layoutManager = FlexboxLayoutManager(requireContext()) + binding?.topicsRecycler?.adapter = topicsAdapter + topicsAdapter.setItems(repoData.topics) + + binding?.createdAt?.text = repoData.created_at.getStringDateFromLong("dd.MM.yyyy HH:mm") + binding?.updatedAt?.text = repoData.updated_at.getStringDateFromLong("dd.MM.yyyy HH:mm") + binding?.repoSize?.text = getString(R.string.disk_usage_template, repoData.diskUsageKB) + + + // Setup languages bar + val langValues = ArrayList() + val langColors = ArrayList() + + repoData.languages.forEach { repoLanguage -> + langValues.add(repoLanguage.size.toFloat()) + langColors.add(Color.parseColor(repoLanguage.color)) + } + + binding?.languagesBar?.setValues(langValues, langColors) + binding?.languagesBar?.invalidate() + + // Setup legend for languages bar (recycler) + val languagesAdapter = RepoLanguagesAdapter() + binding?.languagesRecycler?.layoutManager = FlexboxLayoutManager(requireContext()) + binding?.languagesRecycler?.adapter = languagesAdapter + languagesAdapter.setItems(repoData.languages) + } + }) + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/filters/RepositoriesListFiltersHelper.kt b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/filters/RepositoriesListFiltersHelper.kt index ed30bc6e..f0bf64b6 100644 --- a/app/src/main/java/by/alexandr7035/gitstat/view/repositories/filters/RepositoriesListFiltersHelper.kt +++ b/app/src/main/java/by/alexandr7035/gitstat/view/repositories/filters/RepositoriesListFiltersHelper.kt @@ -46,7 +46,7 @@ object RepositoriesListFiltersHelper { // Remove all repos if language is not from filters' set if (filters.filterLanguages.isNotEmpty()) { filteredList.removeAll { - !filters.filterLanguages.contains(it.language) + !filters.filterLanguages.contains(it.primaryLanguage) } } diff --git a/app/src/main/res/drawable/background_card_clickable.xml b/app/src/main/res/drawable/background_card_clickable.xml new file mode 100644 index 00000000..6e169072 --- /dev/null +++ b/app/src/main/res/drawable/background_card_clickable.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_repository_item.xml b/app/src/main/res/drawable/background_repository_item.xml index 04e50288..d4e4c6ec 100644 --- a/app/src/main/res/drawable/background_repository_item.xml +++ b/app/src/main/res/drawable/background_repository_item.xml @@ -1,11 +1,25 @@ - - - + - - - \ No newline at end of file + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_repository_topic.xml b/app/src/main/res/drawable/background_repository_topic.xml new file mode 100644 index 00000000..a9c5e33c --- /dev/null +++ b/app/src/main/res/drawable/background_repository_topic.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contribution_types.xml b/app/src/main/res/layout/fragment_contribution_types.xml new file mode 100644 index 00000000..6a70f83a --- /dev/null +++ b/app/src/main/res/layout/fragment_contribution_types.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contributions.xml b/app/src/main/res/layout/fragment_contributions.xml index 090848c4..c06b89ec 100644 --- a/app/src/main/res/layout/fragment_contributions.xml +++ b/app/src/main/res/layout/fragment_contributions.xml @@ -122,15 +122,16 @@ @@ -146,26 +147,17 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - diff --git a/app/src/main/res/layout/fragment_about_app.xml b/app/src/main/res/layout/fragment_info.xml similarity index 86% rename from app/src/main/res/layout/fragment_about_app.xml rename to app/src/main/res/layout/fragment_info.xml index 598aef30..58090e74 100644 --- a/app/src/main/res/layout/fragment_about_app.xml +++ b/app/src/main/res/layout/fragment_info.xml @@ -8,7 +8,7 @@ diff --git a/app/src/main/res/layout/fragment_repository_page.xml b/app/src/main/res/layout/fragment_repository_page.xml new file mode 100644 index 00000000..d9dd7d7d --- /dev/null +++ b/app/src/main/res/layout/fragment_repository_page.xml @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_contribution_type_item.xml b/app/src/main/res/layout/view_contribution_type_item.xml new file mode 100644 index 00000000..ef17cbfd --- /dev/null +++ b/app/src/main/res/layout/view_contribution_type_item.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_contribution_types_plot_legend_item.xml b/app/src/main/res/layout/view_contribution_types_plot_legend_item.xml deleted file mode 100644 index f9a90c20..00000000 --- a/app/src/main/res/layout/view_contribution_types_plot_legend_item.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_repository.xml b/app/src/main/res/layout/view_repository.xml index c79cefa3..287d3506 100644 --- a/app/src/main/res/layout/view_repository.xml +++ b/app/src/main/res/layout/view_repository.xml @@ -2,6 +2,7 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_repository_topic.xml b/app/src/main/res/layout/view_repository_topic.xml new file mode 100644 index 00000000..e233c744 --- /dev/null +++ b/app/src/main/res/layout/view_repository_topic.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 9368d4df..5fa367d7 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -11,10 +11,6 @@ android:label="fragment_login" tools:layout="@layout/fragment_login" > - - + + @@ -120,7 +122,7 @@ @@ -167,12 +169,21 @@ app:nullable="false"/> + android:id="@+id/infoAppFragment" + android:name="by.alexandr7035.gitstat.view.info.InfoFragment" + android:label="InfoFragment" + tools:layout="@layout/fragment_info"> + + + + - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 00000000..ab3bd785 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index fefe3c3e..7074f882 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -13,6 +13,10 @@ #595959 #898989 #B1B1B1 + #E1E1E1 + + #437DDC + #4D3872 #FFC107 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e24c8752..86dee6e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ GitStat GitStat v%1$s + GitStat is a simple android app designed to aggregate Github profile data into informative cards and graphs.
@@ -15,9 +16,33 @@ - List of your repositories with filters and sorting
- Contributions summary
- Charts for contributions (types, count per day, contribution rate)
- - Contributions grid (Github-like)
+ - Contributions grid (Github-like) ]]>
+ + Github Oauth (Firebase) for user authentication. + As soon as you click the login button, system opens the browser. + You have to enter the login and password from your Github account. +
+
+ - As Oauth method is used for authentication, even if you log out, you may be able log in again without entering your Github login and password. + Clear your browser cache after logging out if you wish to change GitHub account. +
+
+ - The application gets access to read:user and repo access scopes. +
+
+ - The application does not perform any write operations on your github account. +
+
+ - Full repo scope is used only to have access to your private repos data and not to perform malicious write operations. +
+
+ - The application does not store your GitHub account data anywhere outside the device local storage. + ]]> +
+ github.com \@%s @@ -93,7 +118,6 @@ Check how do we manage your data in the Privacy Policy. Privacy Policy - https://github.com/alexandr7035/gitstat/tree/master#privacy-policy Privacy Policy Refresh @@ -137,5 +161,7 @@ Your %1$d %1$s contribution(s) + %1$d KB + Forked from: %1$s]]>
\ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 531121df..481f441c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -149,4 +149,19 @@ 16sp + + + + \ No newline at end of file