edit issue, markdown layout editor and imgur upload

This commit is contained in:
k0shk0sh 2019-07-29 20:55:47 +02:00
parent 6b26c1ff97
commit 1771991ec9
15 changed files with 428 additions and 16 deletions

View File

@ -9,7 +9,6 @@ import com.fastaccess.github.platform.workmanager.DaggerWorkerFactory
import dagger.BindsInstance
import dagger.Component
import dagger.android.AndroidInjector
import dagger.android.support.AndroidSupportInjectionModule
import dagger.android.support.DaggerApplication
import javax.inject.Singleton
@ -24,10 +23,7 @@ import javax.inject.Singleton
NetworkModule::class,
RepositoryModule::class,
ActivityBindingModule::class,
FragmentBindingModule::class,
DialogFragmentBindingModule::class,
RepositoryModule::class,
AndroidSupportInjectionModule::class]
RepositoryModule::class ]
)
interface AppComponent : AndroidInjector<DaggerApplication> {

View File

@ -11,12 +11,18 @@ import com.fastaccess.github.ui.modules.profile.ProfileActivity
import com.fastaccess.github.ui.modules.trending.TrendingActivity
import dagger.Module
import dagger.android.ContributesAndroidInjector
import dagger.android.support.AndroidSupportInjectionModule
/**
* Created by Kosh on 12.05.18.
*/
@Suppress("unused")
@Module
@Module(
includes = [
AndroidSupportInjectionModule::class,
FragmentBindingModule::class,
DialogFragmentBindingModule::class]
)
abstract class ActivityBindingModule {
@PerActivity @ContributesAndroidInjector abstract fun mainActivity(): MainActivity
@PerActivity @ContributesAndroidInjector abstract fun loginChooser(): LoginChooserActivity

View File

@ -1,6 +1,7 @@
package com.fastaccess.github.di.modules
import com.fastaccess.github.di.scopes.PerFragment
import com.fastaccess.github.ui.modules.editor.dialog.CreateLinkDialogFragment
import com.fastaccess.github.ui.modules.issuesprs.edit.labels.create.CreateLabelFragment
import com.fastaccess.github.ui.modules.issuesprs.edit.milestone.CreateMilestoneDialogFragment
import com.fastaccess.github.ui.modules.issuesprs.filter.FilterIssuesPrsBottomSheet
@ -24,4 +25,5 @@ abstract class DialogFragmentBindingModule {
@PerFragment @ContributesAndroidInjector abstract fun provideFilterIssuesPrsBottomSheet(): FilterIssuesPrsBottomSheet
@PerFragment @ContributesAndroidInjector abstract fun provideFilterSearchBottomSheet(): FilterSearchBottomSheet
@PerFragment @ContributesAndroidInjector abstract fun provideFilterTrendingBottomSheet(): FilterTrendingBottomSheet
@PerFragment @ContributesAndroidInjector abstract fun provideCreateLinkDialogFragment(): CreateLinkDialogFragment
}

View File

@ -64,6 +64,24 @@ class NetworkModule {
.addInterceptor(httpLoggingInterceptor)
.build()
@Named("imgurClient") @Singleton @Provides fun provideHttpClientForImgur(
httpLoggingInterceptor: HttpLoggingInterceptor
): OkHttpClient = OkHttpClient
.Builder()
.addInterceptor(Pandora.get().interceptor)
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val requestBuilder = original.newBuilder()
requestBuilder.header("Authorization", "Client-ID " + BuildConfig.IMGUR_CLIENT_ID)
requestBuilder.method(original.method, original.body)
val request = requestBuilder.build()
return chain.proceed(request)
}
})
.build()
@Named("apolloClient") @Singleton @Provides fun provideHttpClientForApollo(
auth: AuthenticationInterceptor,
httpLoggingInterceptor: HttpLoggingInterceptor
@ -108,6 +126,14 @@ class NetworkModule {
@Singleton @Provides fun provideOrganizationService(retrofit: Retrofit): OrganizationService = retrofit.create(OrganizationService::class.java)
@Singleton @Provides fun provideIssueService(retrofit: Retrofit): IssuePrService = retrofit.create(IssuePrService::class.java)
@Singleton @Provides fun provideRepoService(retrofit: Retrofit): RepoService = retrofit.create(RepoService::class.java)
@Singleton @Provides fun provideImgurService(
retrofit: Retrofit.Builder,
@Named("imgurClient") okHttpClient: OkHttpClient
): ImgurService = retrofit
.baseUrl(BuildConfig.IMGUR_URL)
.client(okHttpClient)
.build()
.create(ImgurService::class.java)
}
class AuthenticationInterceptor(

View File

@ -6,6 +6,7 @@ import com.fastaccess.github.di.annotations.ViewModelKey
import com.fastaccess.github.platform.viewmodel.FastHubViewModelFactory
import com.fastaccess.github.ui.modules.auth.LoginChooserViewModel
import com.fastaccess.github.ui.modules.auth.login.LoginViewModel
import com.fastaccess.github.ui.modules.editor.dialog.UploadPictureViewModel
import com.fastaccess.github.ui.modules.feed.fragment.viewmodel.FeedsViewModel
import com.fastaccess.github.ui.modules.issue.fragment.viewmodel.IssueTimelineViewModel
import com.fastaccess.github.ui.modules.issuesprs.edit.assignees.viewmodel.AssigneesViewModel
@ -96,4 +97,7 @@ abstract class ViewModelModule {
@Binds @IntoMap @ViewModelKey(MilestoneViewModel::class)
abstract fun bindMilestoneViewModel(viewModel: MilestoneViewModel): ViewModel
@Binds @IntoMap @ViewModelKey(UploadPictureViewModel::class)
abstract fun bindUploadPictureViewModel(viewModel: UploadPictureViewModel): ViewModel
}

View File

@ -5,17 +5,23 @@ import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.view.View
import android.widget.EditText
import androidx.core.view.isVisible
import com.fastaccess.data.storage.FastHubSharedPreference
import com.fastaccess.github.R
import com.fastaccess.github.base.BaseFragment
import com.fastaccess.github.base.BaseViewModel
import com.fastaccess.github.extensions.getDrawableCompat
import com.fastaccess.github.extensions.isTrue
import com.fastaccess.github.extensions.show
import com.fastaccess.github.platform.mentions.MentionsPresenter
import com.fastaccess.github.ui.modules.editor.dialog.CreateLinkDialogFragment
import com.fastaccess.github.ui.widget.dialog.IconDialogFragment
import com.fastaccess.github.utils.EXTRA
import com.fastaccess.github.utils.extensions.asString
import com.fastaccess.github.utils.extensions.showKeyboard
import com.fastaccess.markdown.MarkdownProvider
import com.fastaccess.markdown.widget.MarkdownLayout
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy
@ -27,12 +33,15 @@ import javax.inject.Inject
/**
* Created by Kosh on 2019-07-20.
*/
class EditorFragment : BaseFragment(), IconDialogFragment.IconDialogClickListener {
class EditorFragment : BaseFragment(), IconDialogFragment.IconDialogClickListener,
MarkdownLayout.MarkdownLayoutCallback,
CreateLinkDialogFragment.OnLinkSelected {
@Inject lateinit var markwon: Markwon
@Inject lateinit var preference: FastHubSharedPreference
@Inject lateinit var mentionsPresenter: MentionsPresenter
@Inject lateinit var markwonAdapterBuilder: MarkwonAdapter.Builder
override fun viewModel(): BaseViewModel? = null
override fun layoutRes(): Int = R.layout.editor_fragment_layout
@ -44,6 +53,8 @@ class EditorFragment : BaseFragment(), IconDialogFragment.IconDialogClickListene
if (savedInstanceState == null) {
editText.setText(arguments?.getString(EXTRA) ?: "")
}
editText.showKeyboard()
editText.setSelection(editText.asString().length)
setupToolbar(R.string.markdown, R.menu.submit_menu) { item ->
val intent = Intent().apply {
putExtra(EXTRA, editText.asString())
@ -53,7 +64,8 @@ class EditorFragment : BaseFragment(), IconDialogFragment.IconDialogClickListene
}
mentionsPresenter.isMatchParent = true
setToolbarNavigationIcon(R.drawable.ic_clear)
markdownLayout.init(editText)
markdownLayout.layoutCallback = this
markdownLayout.init()
initEditText()
}
@ -78,6 +90,28 @@ class EditorFragment : BaseFragment(), IconDialogFragment.IconDialogClickListene
positive.isTrue { activity?.finish() }
}
override fun provideEditText(): EditText = editText
override fun provideReview(isReview: Boolean) {
if (isReview) {
preview.isVisible = true
markwon.setMarkdown(preview, editText.asString())
} else {
preview.setText("")
preview.isVisible = false
}
}
override fun openLinkDialog(isImage: Boolean) = CreateLinkDialogFragment.newInstance(isImage).show(childFragmentManager)
override fun onLinkSelected(
title: String,
link: String,
isImage: Boolean
) {
//TODO upload pic and get URL
markdownLayout.onLinkSelected(title, link, isImage)
}
private fun initEditText() {
Autocomplete.on<String>(editText)
.with(CharPolicy('@'))

View File

@ -0,0 +1,115 @@
package com.fastaccess.github.ui.modules.editor.dialog
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import com.fastaccess.github.R
import com.fastaccess.github.base.BaseDialogFragment
import com.fastaccess.github.extensions.isTrue
import com.fastaccess.github.extensions.observeNotNull
import com.fastaccess.github.utils.EXTRA
import com.fastaccess.github.utils.extensions.asString
import com.github.dhaval2404.imagepicker.ImagePicker
import kotlinx.android.synthetic.main.create_link_dialog_layout.*
import javax.inject.Inject
/**
* Created by Kosh on 2019-07-29.
*/
class CreateLinkDialogFragment : BaseDialogFragment() {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory).get(UploadPictureViewModel::class.java) }
private val isImage by lazy { arguments?.getBoolean(EXTRA) ?: false }
private var callback: OnLinkSelected? = null
override fun onAttach(context: Context) {
super.onAttach(context)
callback = parentFragment as OnLinkSelected // crash if it isn't ;)
}
override fun onDetach() {
callback = null
super.onDetach()
}
override fun layoutRes(): Int = R.layout.create_link_dialog_layout
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
isImage.isTrue {
selectImage.isVisible = true
selectImage.setOnClickListener {
ImagePicker.with(this)
.start()
}
}
cancel.setOnClickListener { dismiss() }
submit.setOnClickListener {
val title = titleEditText.asString()
val link = linkEditText.asString()
if (title.isNotBlank() && link.isNotBlank()) {
if (isImage) {
viewModel.upload(title, link)
} else {
callback?.onLinkSelected(title, link, false)
dismiss()
}
}
}
observeData()
}
private fun observeData() {
viewModel.progress.observeNotNull(this) {
buttonsLayout.isVisible = !it
progress.isVisible = it
}
viewModel.error.observeNotNull(this) {
Toast.makeText(requireContext().applicationContext, it.message, Toast.LENGTH_LONG).show()
}
viewModel.uploadedFileLiveData.observeNotNull(this) {
val title = titleEditText.asString()
callback?.onLinkSelected(title, it, isImage)
dismiss()
}
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
val file = ImagePicker.getFilePath(data) ?: return
linkEditText.setText(file)
}
}
companion object {
fun newInstance(isImage: Boolean = false) = CreateLinkDialogFragment().apply {
arguments = bundleOf(EXTRA to isImage)
}
}
interface OnLinkSelected {
fun onLinkSelected(
title: String,
link: String,
isImage: Boolean
)
}
}

View File

@ -0,0 +1,42 @@
package com.fastaccess.github.ui.modules.editor.dialog
import androidx.lifecycle.MutableLiveData
import com.fastaccess.data.model.FastHubErrors
import com.fastaccess.data.repository.SchedulerProvider
import com.fastaccess.domain.repository.services.ImgurService
import com.fastaccess.github.base.BaseViewModel
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import javax.inject.Inject
/**
* Created by Kosh on 2019-07-29.
*/
class UploadPictureViewModel @Inject constructor(
private val service: ImgurService,
private val scheduler: SchedulerProvider
) : BaseViewModel() {
val uploadedFileLiveData = MutableLiveData<String>()
fun upload(
title: String,
path: String
) {
val image = File(path).asRequestBody("image/*".toMediaTypeOrNull())
justSubscribe(
service.postImage(title, image)
.subscribeOn(scheduler.ioThread())
.observeOn(scheduler.uiThread())
.doOnNext {
val link = it.data?.link
if (!link.isNullOrEmpty()) {
uploadedFileLiveData.postValue(link)
} else {
error.postValue(FastHubErrors(FastHubErrors.ErrorType.OTHER))
}
}
)
}
}

View File

@ -263,7 +263,9 @@ class IssueFragment : BaseFragment(), LockUnlockFragment.OnLockReasonSelected,
opener.text = SpannableBuilder.builder()
.bold(model.author?.login)
.space()
.append(getString(R.string.opened_this_issue))
.space()
.append(model.createdAt?.timeAgo())
userIcon.loadAvatar(model.author?.avatarUrl, model.author?.url ?: "")

View File

@ -46,6 +46,7 @@ class EditIssuePrFragment : BaseFragment() {
toolbar.subtitle = "${model.login}/${model.repo}/${getString(R.string.issue)}${if (model.isCreate) "" else "#${model.number}"}"
setToolbarNavigationIcon(R.drawable.ic_clear)
toolbar.inflateMenu(R.menu.submit_menu)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
if (savedInstanceState == null) {
titleEditText.setText(model.title)

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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"
style="@style/CardViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="15dp">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/titleInput"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/titleEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/linkInput"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_xs_large"
android:layout_marginBottom="@dimen/spacing_xs_large"
android:hint="@string/link">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/linkEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:maxLines="3" />
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<HorizontalScrollView
android:id="@+id/buttonsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/selectImage"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center"
android:text="@string/select"
android:textColor="@color/material_orange_700"
android:visibility="gone"
app:icon="@drawable/ic_image"
app:iconTint="@color/material_orange_700"
tools:visibility="visible" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center"
android:layout_marginEnd="@dimen/spacing_xs_large"
android:text="@string/cancel"
android:textColor="?android:textColorPrimary"
app:icon="@drawable/ic_clear"
app:iconTint="?android:textColorPrimary" />
<com.google.android.material.button.MaterialButton
android:id="@+id/submit"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center"
android:text="@string/submit"
app:icon="@drawable/ic_done" />
</LinearLayout>
</HorizontalScrollView>
<ProgressBar
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</com.google.android.material.card.MaterialCardView>

View File

@ -57,14 +57,15 @@ ext {
]
extrasLibraries = [
"com.airbnb:deeplinkdispatch:$deepLinkDispatch",
"com.evernote:android-state:$androidState",
'com.github.daniel-stoneuk:material-about-library:2.1.0',
'com.scottyab:secure-preferences-lib:0.1.7',
"com.airbnb:deeplinkdispatch:$deepLinkDispatch",
'com.airbnb.android:lottie:3.0.7',
"com.evernote:android-state:$androidState",
'org.jsoup:jsoup:1.12.1',
'com.github.k0shk0sh:RetainedDateTimePickers:1.0.2',
'com.otaliastudios:autocomplete:1.1.0'
'com.otaliastudios:autocomplete:1.1.0',
'com.github.dhaval2404:imagepicker:1.3'
]
networking = [

View File

@ -0,0 +1,18 @@
package com.fastaccess.domain.repository.services
import com.fastaccess.domain.response.ImgureResponseModel
import io.reactivex.Observable
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Query
/**
* Created by Kosh on 2019-07-29.
*/
interface ImgurService {
@POST("image")
fun postImage(@Query("title") title: String, @Body body: RequestBody): Observable<ImgureResponseModel>
}

View File

@ -0,0 +1,18 @@
package com.fastaccess.domain.response
import com.google.gson.annotations.SerializedName
/**
* Created by Kosh on 2019-07-29.
*/
data class ImgureResponseModel(
@SerializedName("success") var isSuccess: Boolean? = null,
@SerializedName("status") var status: Int? = null,
@SerializedName("data") var data: ImgurImage? = null
)
data class ImgurImage(
@SerializedName("title") var title: String? = null,
@SerializedName("description") var description: String? = null,
@SerializedName("link") var link: String? = null
)

View File

@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import androidx.core.view.isVisible
import com.fastaccess.markdown.MarkdownProvider
import com.fastaccess.markdown.R
import kotlinx.android.synthetic.main.markdown_buttons_layout.view.*
@ -14,7 +15,8 @@ import kotlinx.android.synthetic.main.markdown_buttons_layout.view.*
*/
class MarkdownLayout : LinearLayout, View.OnClickListener {
private lateinit var editText: EditText
lateinit var layoutCallback: MarkdownLayoutCallback
constructor(context: Context?) : super(context)
constructor(
@ -35,8 +37,7 @@ class MarkdownLayout : LinearLayout, View.OnClickListener {
if (isInEditMode) return
}
fun init(editText: EditText) {
this.editText = editText
fun init() {
headerOne.setOnClickListener(this)
headerTwo.setOnClickListener(this)
headerThree.setOnClickListener(this)
@ -55,10 +56,12 @@ class MarkdownLayout : LinearLayout, View.OnClickListener {
inlineCode.setOnClickListener(this)
addEmoji.setOnClickListener(this)
signature.setOnClickListener(this)
view.setOnClickListener(this)
}
override fun onClick(v: View?) {
val editText = layoutCallback.provideEditText()
if (editText.selectionEnd == -1 || editText.selectionStart == -1) {
return
}
@ -75,12 +78,36 @@ class MarkdownLayout : LinearLayout, View.OnClickListener {
R.id.code -> MarkdownProvider.addCode(editText)
R.id.numbered -> MarkdownProvider.addList(editText, "1")
R.id.quote -> MarkdownProvider.addQuote(editText)
R.id.link -> MarkdownProvider.addLink(editText, "", "")
R.id.image -> MarkdownProvider.addPhoto(editText, "", "")
R.id.link -> layoutCallback.openLinkDialog()
R.id.image -> layoutCallback.openLinkDialog(true)
R.id.unCheckbox -> MarkdownProvider.addList(editText, "- [x]")
R.id.checkbox -> MarkdownProvider.addList(editText, "- [ ]")
R.id.inlineCode -> MarkdownProvider.addInlinleCode(editText)
R.id.view -> {
val isPreview = editText.isVisible
editText.isVisible = !isPreview
layoutCallback.provideReview(isPreview)
}
}
}
}
fun onLinkSelected(
title: String,
link: String,
isImage: Boolean
) {
val editText = layoutCallback.provideEditText()
if (isImage) {
MarkdownProvider.addPhoto(editText, title, link)
} else {
MarkdownProvider.addLink(editText, title, link)
}
}
interface MarkdownLayoutCallback {
fun provideEditText(): EditText
fun provideReview(isReview: Boolean)
fun openLinkDialog(isImage: Boolean = false)
}
}