Commit 11e088e1 authored by Konstantin Tskhovrebov's avatar Konstantin Tskhovrebov 🤖

Merge branch 'bug/project_info_loading_readme' into 'develop'

New project info screen structure

See merge request !74
parents 583c1d9a a0a5166b
......@@ -23,7 +23,7 @@ object Screens {
const val APP_LIBRARIES_SCREEN = "app libraries screen"
const val PROJECT_FLOW = "project flow"
const val PROJECT_INFO_SCREEN = "project info screen"
const val PROJECT_SCREEN = "project screen"
const val USER_FLOW = "user flow"
const val USER_INFO_SCREEN = "user info screen"
......
package ru.terrakok.gitlabclient.entity
import com.google.gson.annotations.SerializedName
/**
* @author Eugene Shapovalov (CraggyHaggy). Date: 06.02.18
*/
data class RepositoryTreeNode(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("type") val type: RepositoryTreeNodeType,
@SerializedName("path") val path: String,
@SerializedName("mode") val mode: String
)
\ No newline at end of file
package ru.terrakok.gitlabclient.entity
import com.google.gson.annotations.SerializedName
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 08.02.18.
*/
enum class RepositoryTreeNodeType(private val jsonName: String) {
@SerializedName("tree") TREE("tree"),
@SerializedName("blob") BLOB("blob");
override fun toString() = jsonName
}
\ No newline at end of file
......@@ -114,7 +114,7 @@ fun ImageView.loadRoundedImage(
}
fun TargetHeader.openInfo(router: FlowRouter) {
when(target) {
when (target) {
AppTarget.PROJECT -> {
router.startFlow(Screens.PROJECT_FLOW, targetId)
}
......
......@@ -66,6 +66,14 @@ interface GitlabApi {
@Query("ref") branchName: String
): Single<File>
@GET("$API_PATH/projects/{id}/repository/tree")
fun getRepositoryTree(
@Path("id") id: Long,
@Query("path") path: String?,
@Query("ref") branchName: String?,
@Query("recursive") recursive: Boolean?
): Single<List<RepositoryTreeNode>>
@GET("$API_PATH/user")
fun getMyUser(): Single<User>
......
package ru.terrakok.gitlabclient.model.interactor.project
import io.reactivex.Single
import ru.terrakok.gitlabclient.entity.OrderBy
import ru.terrakok.gitlabclient.model.repository.project.ProjectRepository
import ru.terrakok.gitlabclient.model.repository.tools.Base64Tools
......@@ -41,9 +42,28 @@ class ProjectInteractor @Inject constructor(
fun getProject(id: Long) = projectRepository.getProject(id)
/**
* Returns [Single] with the project readme file decoded as [String]
* at the specified project and branch. The project readme is searched ignoring case.
*
* @param id project id.
* @param branchName project branch name to search readme.
* @return [Single] with readme decoded as [String].
* @throws NoSuchElementException if project readme with name [README_FILE_NAME] not found.
*/
fun getProjectReadme(id: Long, branchName: String) =
projectRepository.getFile(id, "README.md", branchName)
.observeOn(schedulers.computation())
.map { file -> base64Tools.decode(file.content) }
.observeOn(schedulers.ui())
projectRepository.getRepositoryTree(projectId = id, branchName = branchName)
.map { treeNodes ->
treeNodes.first { it.name.contains(README_FILE_NAME, true) }
}
.flatMap { treeNode ->
projectRepository.getFile(id, treeNode.name, branchName)
.observeOn(schedulers.computation())
.map { file -> base64Tools.decode(file.content) }
.observeOn(schedulers.ui())
}
companion object {
private const val README_FILE_NAME = "readme.md"
}
}
\ No newline at end of file
......@@ -83,7 +83,7 @@ class EventRepository @Inject constructor(
event.author.name,
event.actionName,
targetData.name,
project?.nameWithNamespace ?: ""
project?.name ?: ""
),
getBody(event),
event.createdAt,
......
......@@ -79,7 +79,7 @@ class IssueRepository @Inject constructor(
issue.author.name,
EventAction.CREATED,
"${AppTarget.ISSUE} #${issue.iid}",
project.nameWithNamespace
project.name
),
issue.title,
issue.createdAt,
......
......@@ -86,7 +86,7 @@ class MergeRequestRepository @Inject constructor(
mr.author.name,
EventAction.CREATED,
"${AppTarget.MERGE_REQUEST} !${mr.iid}",
project.nameWithNamespace
project.name
),
mr.title,
mr.createdAt,
......
......@@ -61,4 +61,14 @@ class ProjectRepository @Inject constructor(
.getFile(projectId, path, branchName)
.subscribeOn(schedulers.io())
.observeOn(schedulers.ui())
fun getRepositoryTree(
projectId: Long,
path: String? = null,
branchName: String? = null,
recursive: Boolean? = null
) = api
.getRepositoryTree(projectId, path, branchName, recursive)
.subscribeOn(schedulers.io())
.observeOn(schedulers.ui())
}
\ No newline at end of file
package ru.terrakok.gitlabclient.presentation.project
import com.arellomobile.mvp.InjectViewState
import io.reactivex.Single
import ru.terrakok.cicerone.Router
import ru.terrakok.gitlabclient.model.interactor.project.ProjectInteractor
import ru.terrakok.gitlabclient.presentation.global.BasePresenter
......@@ -27,17 +28,24 @@ class ProjectInfoPresenter @Inject constructor(
override fun onFirstViewAttach() {
super.onFirstViewAttach()
projectInteractor.getProject(projectId)
.doOnSuccess { project -> viewState.showProjectInfo(project) }
projectInteractor
.getProject(projectId)
.flatMap { project ->
projectInteractor
.getProjectReadme(project.id, project.defaultBranch)
.onErrorResumeNext { throwable ->
when (throwable) {
is NoSuchElementException -> Single.just("")
else -> Single.error(throwable)
}
}
.flatMap { mdConverter.markdownToSpannable(it) }
.map { mdReadme -> Pair(project, mdReadme) }
}
.doOnSubscribe { viewState.showProgress(true) }
.doAfterTerminate { viewState.showProgress(false) }
.subscribe(
{ viewState.showReadmeFile(it) },
{ (project, mdReadme) -> viewState.showProject(project, mdReadme) },
{ errorHandler.proceed(it, { viewState.showMessage(it) }) }
)
.connect()
......
......@@ -11,9 +11,8 @@ import ru.terrakok.gitlabclient.entity.Project
*/
@StateStrategyType(AddToEndSingleStrategy::class)
interface ProjectInfoView : MvpView {
fun showProjectInfo(project: Project)
fun showProject(project: Project, mdReadme: CharSequence)
fun showProgress(show: Boolean)
fun showReadmeFile(mdReadme: CharSequence)
@StateStrategyType(OneExecutionStateStrategy::class)
fun showMessage(message: String)
......
package ru.terrakok.gitlabclient.presentation.projects
import com.arellomobile.mvp.InjectViewState
import ru.terrakok.cicerone.Router
import ru.terrakok.gitlabclient.Screens
import ru.terrakok.gitlabclient.entity.Project
import ru.terrakok.gitlabclient.model.interactor.project.ProjectInteractor
......
......@@ -11,7 +11,6 @@ import ru.terrakok.gitlabclient.toothpick.DI
import ru.terrakok.gitlabclient.toothpick.PrimitiveWrapper
import ru.terrakok.gitlabclient.toothpick.qualifier.ProjectId
import ru.terrakok.gitlabclient.ui.global.BaseActivity
import ru.terrakok.gitlabclient.ui.project.info.ProjectInfoFragment
import toothpick.Toothpick
import toothpick.config.Module
......@@ -38,7 +37,7 @@ class ProjectActivity : BaseActivity() {
}
if (savedInstanceState == null) {
navigator.setLaunchScreen(Screens.PROJECT_INFO_SCREEN, null)
navigator.setLaunchScreen(Screens.PROJECT_SCREEN, null)
}
}
......@@ -51,7 +50,7 @@ class ProjectActivity : BaseActivity() {
override val navigator = object : FlowNavigator(this, R.id.container) {
override fun createFragment(screenKey: String?, data: Any?): Fragment? = when (screenKey) {
Screens.PROJECT_INFO_SCREEN -> ProjectInfoFragment()
Screens.PROJECT_SCREEN -> ProjectFragment()
else -> null
}
}
......
package ru.terrakok.gitlabclient.ui.project
import android.os.Bundle
import com.aurelhubert.ahbottomnavigation.AHBottomNavigationAdapter
import kotlinx.android.synthetic.main.fragment_project.*
import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.extension.color
import ru.terrakok.gitlabclient.ui.global.BaseFragment
import ru.terrakok.gitlabclient.ui.project.info.ProjectInfoFragment
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 10.02.18.
*/
class ProjectFragment : BaseFragment() {
override val layoutRes: Int = R.layout.fragment_project
private lateinit var tabs: HashMap<String, BaseFragment>
private val tabKeys = listOf(
tabIdToFragmentTag(R.id.tab_info),
tabIdToFragmentTag(R.id.tab_issue),
tabIdToFragmentTag(R.id.tab_merge_request)
)
private fun tabIdToFragmentTag(id: Int) = "tab_$id"
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
AHBottomNavigationAdapter(activity, R.menu.project_bottom_menu).apply {
setupWithBottomNavigation(bottomBar)
}
with(bottomBar) {
accentColor = context.color(R.color.colorPrimary)
inactiveColor = context.color(R.color.silver)
setOnTabSelectedListener { position, wasSelected ->
if (!wasSelected) showTab(position, currentItem)
true
}
}
if (savedInstanceState == null) {
tabs = createNewFragments()
childFragmentManager.beginTransaction()
.add(R.id.container, tabs[tabKeys[0]], tabKeys[0])
.commitNow()
bottomBar.setCurrentItem(0, false)
} else {
tabs = findFragments()
}
}
private fun showTab(newItem: Int, oldItem: Int) {
childFragmentManager.beginTransaction()
.hide(tabs[tabKeys[oldItem]])
.show(tabs[tabKeys[newItem]])
.commit()
}
private fun createNewFragments(): HashMap<String, BaseFragment> = hashMapOf(
tabKeys[0] to ProjectInfoFragment()
)
private fun findFragments(): HashMap<String, BaseFragment> = hashMapOf(
tabKeys[0] to childFragmentManager.findFragmentByTag(tabKeys[0]) as BaseFragment
)
}
\ No newline at end of file
......@@ -10,6 +10,7 @@ import ru.terrakok.gitlabclient.entity.Project
import ru.terrakok.gitlabclient.entity.Visibility
import ru.terrakok.gitlabclient.extension.loadRoundedImage
import ru.terrakok.gitlabclient.extension.shareText
import ru.terrakok.gitlabclient.extension.visible
import ru.terrakok.gitlabclient.presentation.project.ProjectInfoPresenter
import ru.terrakok.gitlabclient.presentation.project.ProjectInfoView
import ru.terrakok.gitlabclient.toothpick.DI
......@@ -22,10 +23,11 @@ import toothpick.Toothpick
class ProjectInfoFragment : BaseFragment(), ProjectInfoView {
override val layoutRes = R.layout.fragment_project_info
private var project: Project? = null
@InjectPresenter lateinit var presenter: ProjectInfoPresenter
private var project: Project? = null
@ProvidePresenter
fun providePresenter(): ProjectInfoPresenter =
Toothpick
......@@ -46,29 +48,29 @@ class ProjectInfoFragment : BaseFragment(), ProjectInfoView {
}
}
override fun showProjectInfo(project: Project) {
override fun showProject(project: Project, mdReadme: CharSequence) {
this.project = project
toolbar.title = project.name
titleTextView.text = project.nameWithNamespace
descriptionTextView.text = project.description
starsTextView.text = project.starCount.toString()
forksTextView.text = project.forksCount.toString()
avatarImageView.loadRoundedImage(project.avatarUrl, context)
iconImageView.setBackgroundResource(R.drawable.circle)
iconImageView.setImageResource(when (project.visibility) {
Visibility.PRIVATE -> R.drawable.ic_lock_white_18dp
Visibility.INTERNAL -> R.drawable.ic_security_white_24dp
else -> R.drawable.ic_globe_18dp
})
starsTextView.text = project.starCount.toString()
forksTextView.text = project.forksCount.toString()
Markwon.setText(readmeTextView, mdReadme)
}
override fun showProgress(show: Boolean) {
showProgressDialog(show)
}
override fun showReadmeFile(mdReadme: CharSequence) {
Markwon.setText(readmeTextView, mdReadme)
fullscreenProgressView.visible(show)
projectInfoLayout.visible(!show)
}
override fun showMessage(message: String) {
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.aurelhubert.ahbottomnavigation.AHBottomNavigation
android:id="@+id/bottomBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:elevation="8dp" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
......@@ -12,21 +12,21 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_arrow_back_24dp"
tools:title="Username"/>
tools:title="GitFox" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:id="@+id/projectInfoLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="match_parent">
<ImageView
android:id="@+id/avatarImageView"
......@@ -35,7 +35,7 @@
android:layout_margin="16dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/default_img"/>
tools:src="@drawable/default_img" />
<ImageView
android:id="@+id/iconImageView"
......@@ -43,12 +43,11 @@
android:layout_height="18dp"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:background="@drawable/circle"
android:padding="2dp"
app:layout_constraintLeft_toLeftOf="@id/avatarImageView"
app:layout_constraintTop_toTopOf="@id/avatarImageView"
app:tint="@color/white"
tools:src="@drawable/ic_security_white_24dp"/>
tools:src="@drawable/ic_security_white_24dp" />
<TextView
android:id="@+id/titleTextView"
......@@ -64,7 +63,7 @@
app:layout_constraintLeft_toRightOf="@+id/avatarImageView"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/avatarImageView"
tools:text="GitLab Community Edition"/>
tools:text="GitLab Community Edition" />
<TextView
android:id="@+id/forksTextView"
......@@ -79,7 +78,7 @@
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="@id/titleTextView"
app:layout_constraintTop_toBottomOf="@id/titleTextView"
tools:text="2312"/>
tools:text="34" />
<TextView
android:id="@+id/starsTextView"
......@@ -95,7 +94,7 @@
android:textSize="16sp"
app:layout_constraintLeft_toRightOf="@id/forksTextView"
app:layout_constraintTop_toBottomOf="@id/titleTextView"
tools:text="2312"/>
tools:text="68" />
<TextView
android:id="@+id/descriptionTextView"
......@@ -109,26 +108,38 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/avatarImageView"
tools:text="GitLab Community Edition (CE) is an open source end-to-end software development platform with built-in version control, issue tracking, code review, CI/CD, and more. Self-host GitLab CE on your own servers, in a container, or on a cloud provider."/>
tools:text="GitLab Community Edition (CE) is an open source end-to-end software development platform with built-in version control, issue tracking, code review, CI/CD, and more. Self-host GitLab CE on your own servers, in a container, or on a cloud provider." />
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="@dimen/divider_size"
android:layout_marginTop="16dp"
android:background="@color/divider"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView"/>
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
<TextView
android:id="@+id/readmeTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:linksClickable="true"
android:padding="16dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
</android.support.constraint.ConstraintLayout>
<TextView
android:id="@+id/readmeTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"/>
<include
android:id="@+id/fullscreenProgressView"
layout="@layout/layout_gitlab_progress"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center" />
</LinearLayout>
</FrameLayout>
</ScrollView>
......
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/tab_info"
android:icon="@drawable/ic_info_black_24dp"
android:title="@string/menu_project_info" />
<item
android:id="@+id/tab_issue"
android:icon="@drawable/ic_issues"
android:title="@string/menu_project_issue" />
<item
android:id="@+id/tab_merge_request"
android:icon="@drawable/ic_merge"
android:title="@string/menu_project_merge_request" />
</menu>
\ No newline at end of file
......@@ -148,6 +148,11 @@
<!--Issue Info screen-->
<string name="issue_info_subtitle" formatted="false">%s by %s %s</string>
<!--Project info screen-->
<string name="menu_project_info">Info</string>
<string name="menu_project_issue">Issues</string>
<string name="menu_project_merge_request">Merge Requests</string>
<!--Calligraphy Config-->
<string name="font_main_regular">fonts/Roboto-Regular.ttf</string>
<string name="font_main_medium">fonts/Roboto-Medium.ttf</string>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment