Skip to content

Commit 08e0dfd

Browse files
author
ADFA
committed
Daily merge from stage to main
2 parents 9e8a19b + 09ffb0d commit 08e0dfd

66 files changed

Lines changed: 1994 additions & 850 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

actions/src/main/java/com/itsaky/androidide/actions/actionUtils.kt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,23 @@ import io.github.rosemoe.sora.widget.CodeEditor
2424
import java.io.File
2525
import java.nio.file.Path
2626

27-
fun ActionData.getContext(): Context? {
28-
return get(Context::class.java)
29-
}
27+
inline fun <reified T> ActionData.get(): T?=
28+
get(T::class.java)
3029

31-
fun ActionData.requireContext(): Context {
32-
return getContext() ?: throw IllegalArgumentException("No context instance provided")
33-
}
30+
inline fun <reified T> ActionData.require(): T =
31+
checkNotNull(get<T>())
3432

35-
fun ActionData.requireFile(): File {
36-
return get(File::class.java) ?: throw IllegalArgumentException("No file instance provided")
37-
}
33+
inline fun <reified T> ActionData.has(): Boolean =
34+
get<T>() != null
35+
36+
fun ActionData.getContext(): Context? =
37+
get<Context>()
38+
39+
fun ActionData.requireContext(): Context =
40+
require<Context>()
41+
42+
fun ActionData.requireFile(): File =
43+
require<File>()
3844

3945
fun ActionData.requirePath(): Path {
4046
return requireFile().toPath()

app/src/main/java/com/itsaky/androidide/di/AppModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.itsaky.androidide.analytics.AnalyticsManager
88
import com.itsaky.androidide.analytics.IAnalyticsManager
99
import com.itsaky.androidide.git.core.GitCredentialsManager
1010
import com.itsaky.androidide.roomData.recentproject.RecentProjectRoomDatabase
11+
import com.itsaky.androidide.viewmodel.CloneRepositoryViewModel
1112
import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel
1213
import com.itsaky.androidide.viewmodel.MainViewModel
1314
import kotlinx.coroutines.CoroutineScope
@@ -33,6 +34,7 @@ val coreModule =
3334
GitBottomSheetViewModel(get())
3435
}
3536
viewModel { MainViewModel(get()) }
37+
viewModel { CloneRepositoryViewModel(get(), get()) }
3638

3739

3840
single<CoroutineScope> {

app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import android.view.ViewGroup
77
import android.widget.EditText
88
import androidx.core.widget.doAfterTextChanged
99
import org.koin.androidx.viewmodel.ext.android.activityViewModel
10-
import androidx.fragment.app.viewModels
1110
import androidx.lifecycle.Lifecycle
1211
import androidx.lifecycle.lifecycleScope
1312
import androidx.lifecycle.repeatOnLifecycle
@@ -22,15 +21,19 @@ import com.itsaky.androidide.R
2221
import com.itsaky.androidide.dnd.handleGitUrlDrop
2322
import com.itsaky.androidide.idetooltips.TooltipManager
2423
import com.itsaky.androidide.idetooltips.TooltipTag
24+
import com.itsaky.androidide.utils.flashError
25+
import com.itsaky.androidide.utils.flashInfo
2526
import com.itsaky.androidide.utils.forEachViewRecursively
2627
import kotlinx.coroutines.launch
28+
import org.koin.androidx.viewmodel.ext.android.viewModel
2729
import java.io.File
2830

2931
class CloneRepositoryFragment : BaseFragment() {
3032

3133
private var binding: FragmentCloneRepositoryBinding? = null
32-
private val viewModel: CloneRepositoryViewModel by viewModels()
34+
private val viewModel: CloneRepositoryViewModel by viewModel()
3335
private val mainViewModel: MainViewModel by activityViewModel()
36+
private var lastStatusResId: Int? = null
3437

3538
override fun onCreateView(
3639
inflater: LayoutInflater,
@@ -161,16 +164,21 @@ class CloneRepositoryFragment : BaseFragment() {
161164
isEnabled = state.isCloneButtonEnabled
162165
refreshStatus(isForRetry = false)
163166
}
164-
statusText.text = ""
167+
lastStatusResId = null
165168
}
166169

167170
is CloneRepoUiState.Cloning -> {
168171
cloneButton.apply {
169172
isEnabled = false
170173
refreshStatus(isForRetry = false)
171174
}
172-
statusText.text = state.statusTextResId?.let { getString(it) }
173-
?: getString(R.string.cloning_repo)
175+
176+
if (state.statusTextResId != lastStatusResId) {
177+
lastStatusResId = state.statusTextResId
178+
val message = state.statusTextResId?.let { getString(it) }
179+
?: getString(R.string.cloning_repo)
180+
flashInfo(message)
181+
}
174182
}
175183

176184
is CloneRepoUiState.Error -> {
@@ -180,12 +188,12 @@ class CloneRepositoryFragment : BaseFragment() {
180188
}
181189
val statusMessage =
182190
state.errorResId?.let { getString(it) } ?: state.errorMessage
183-
statusText.text = statusMessage
191+
flashError(statusMessage)
184192
}
185193

186194
is CloneRepoUiState.Success -> {
187195
cloneButton.isEnabled = true
188-
statusText.text = getString(R.string.clone_successful)
196+
flashInfo(getString(R.string.clone_successful))
189197
val destDir = File(state.localPath)
190198
if (destDir.exists()) {
191199
mainViewModel.setScreen(MainViewModel.SCREEN_MAIN)

app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.itsaky.androidide.databinding.FragmentGitBottomSheetBinding
2222
import com.itsaky.androidide.fragments.git.adapter.GitFileChangeAdapter
2323
import com.itsaky.androidide.git.core.GitCredentialsManager
2424
import com.itsaky.androidide.git.core.models.ChangeType
25+
import com.itsaky.androidide.interfaces.IEditorHandler
2526
import com.itsaky.androidide.preferences.internal.GitPreferences
2627
import com.itsaky.androidide.utils.flashSuccess
2728
import com.itsaky.androidide.viewmodel.BottomSheetViewModel
@@ -52,7 +53,6 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
5253
onFileClicked = { change ->
5354
when (change.type) {
5455
ChangeType.CONFLICTED -> {
55-
// Open conflicted file in editor
5656
val activity = requireActivity()
5757
if (activity is EditorHandlerActivity) {
5858
viewLifecycleOwner.lifecycleScope.launch {
@@ -66,14 +66,16 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
6666
}
6767
}
6868
else -> {
69-
// Show diff in a dialog when changed file is clicked
7069
val dialog = GitDiffViewerDialog.newInstance(change.path)
7170
dialog.show(childFragmentManager, "GitDiffViewerDialog")
7271
}
7372
}
7473
},
7574
onSelectionChanged = {
7675
validateCommitButton()
76+
},
77+
onResolveConflict = { change ->
78+
viewModel.resolveConflict(change.path)
7779
}
7880
)
7981

@@ -183,19 +185,21 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
183185
}
184186

185187
binding.commitButton.setOnClickListener {
186-
val summary = binding.commitSummary.text?.toString()?.trim() ?: ""
187-
val description = binding.commitDescription.text?.toString()?.trim()
188-
189-
if (summary.isNotEmpty() && fileChangeAdapter.selectedFiles.isNotEmpty() && hasAuthorInfo()) {
190-
viewModel.commitChanges(
191-
summary = summary,
192-
description = description,
193-
selectedPaths = fileChangeAdapter.selectedFiles.toList()
194-
) {
195-
// Clear the inputs on successful commit
196-
binding.commitSummary.text?.clear()
197-
binding.commitDescription.text?.clear()
198-
fileChangeAdapter.selectedFiles.clear()
188+
checkUnsavedChangesAndProceed {
189+
val summary = binding.commitSummary.text?.toString()?.trim() ?: ""
190+
val description = binding.commitDescription.text?.toString()?.trim()
191+
192+
if (summary.isNotEmpty() && fileChangeAdapter.selectedFiles.isNotEmpty() && hasAuthorInfo()) {
193+
viewModel.commitChanges(
194+
summary = summary,
195+
description = description,
196+
selectedPaths = fileChangeAdapter.selectedFiles.toList()
197+
) {
198+
// Clear the inputs on successful commit
199+
binding.commitSummary.text?.clear()
200+
binding.commitDescription.text?.clear()
201+
fileChangeAdapter.selectedFiles.clear()
202+
}
199203
}
200204
}
201205
}
@@ -303,12 +307,14 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
303307
}
304308

305309
binding.btnPull.setOnClickListener {
306-
val username = credentialsManager.getUsername()
307-
val token = credentialsManager.getToken()
308-
if (!username.isNullOrBlank() && !token.isNullOrBlank()) {
309-
viewModel.pull(username, token)
310-
} else {
311-
showCredentialsDialog()
310+
checkUnsavedChangesAndProceed {
311+
val username = credentialsManager.getUsername()
312+
val token = credentialsManager.getToken()
313+
if (!username.isNullOrBlank() && !token.isNullOrBlank()) {
314+
viewModel.pull(username, token)
315+
} else {
316+
showCredentialsDialog()
317+
}
312318
}
313319
}
314320
}
@@ -344,6 +350,25 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
344350
}
345351
}
346352

353+
private fun checkUnsavedChangesAndProceed(action: () -> Unit) {
354+
val handler = requireActivity() as? IEditorHandler
355+
if (handler?.areFilesModified() == true) {
356+
MaterialAlertDialogBuilder(requireContext())
357+
.setTitle(R.string.title_files_unsaved)
358+
.setMessage(R.string.msg_save_before_git_action)
359+
.setPositiveButton(R.string.save_before_git_action) { _, _ ->
360+
handler.saveAllAsync { action() }
361+
}
362+
.setNegativeButton(R.string.no_save_before_git_action) { _, _ ->
363+
action()
364+
}
365+
.setNeutralButton(android.R.string.cancel, null)
366+
.show()
367+
} else {
368+
action()
369+
}
370+
}
371+
347372
override fun onDestroyView() {
348373
super.onDestroyView()
349374
_binding = null

app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.itsaky.androidide.fragments.git.adapter
22

33
import android.view.LayoutInflater
4+
import android.view.View
45
import android.view.ViewGroup
56
import androidx.recyclerview.widget.DiffUtil
67
import androidx.recyclerview.widget.ListAdapter
@@ -12,7 +13,8 @@ import com.itsaky.androidide.git.core.models.FileChange
1213

1314
class GitFileChangeAdapter(
1415
private val onFileClicked: (FileChange) -> Unit,
15-
private val onSelectionChanged: (Int) -> Unit = {}
16+
private val onSelectionChanged: (Int) -> Unit = {},
17+
private val onResolveConflict: (FileChange) -> Unit = {}
1618
) : ListAdapter<FileChange, GitFileChangeAdapter.ViewHolder>(DiffCallback()) {
1719

1820
// Keep track of which files are selected to be committed
@@ -40,24 +42,59 @@ class GitFileChangeAdapter(
4042
}
4143
}
4244

43-
binding.checkbox.setOnCheckedChangeListener { _, isChecked ->
45+
binding.btnMarkResolved.setOnClickListener {
4446
val pos = bindingAdapterPosition
4547
if (pos != RecyclerView.NO_POSITION) {
46-
val change = getItem(pos)
47-
if (isChecked) {
48-
selectedFiles.add(change.path)
49-
} else {
50-
selectedFiles.remove(change.path)
51-
}
52-
onSelectionChanged(selectedFiles.size)
48+
onResolveConflict(getItem(pos))
5349
}
5450
}
5551
}
5652

5753
fun bind(change: FileChange) {
5854
binding.filePath.text = change.path
5955

56+
val isConflicted = change.type == ChangeType.CONFLICTED
57+
58+
// Clear listener before setting state to avoid recursive/accidental calls during binding
59+
binding.checkbox.setOnCheckedChangeListener(null)
60+
61+
binding.checkbox.apply {
62+
isEnabled = !isConflicted
63+
visibility = if (isConflicted) View.INVISIBLE else View.VISIBLE
64+
}
65+
binding.btnMarkResolved.visibility = if (isConflicted) View.VISIBLE else View.GONE
66+
67+
// Ensure conflicted files are never in selectedFiles
68+
if (isConflicted && selectedFiles.remove(change.path)) {
69+
onSelectionChanged(selectedFiles.size)
70+
}
71+
6072
binding.checkbox.isChecked = selectedFiles.contains(change.path)
73+
74+
// Re-set the listener after the state is initialized
75+
binding.checkbox.setOnCheckedChangeListener { _, isChecked ->
76+
val pos = bindingAdapterPosition
77+
if (pos == RecyclerView.NO_POSITION) return@setOnCheckedChangeListener
78+
79+
val changeAtPos = getItem(pos)
80+
// Conflicted files should not be selectable
81+
if (changeAtPos.type == ChangeType.CONFLICTED) {
82+
binding.checkbox.isChecked = false
83+
return@setOnCheckedChangeListener
84+
}
85+
86+
if (isChecked) {
87+
selectedFiles.add(changeAtPos.path)
88+
} else {
89+
selectedFiles.remove(changeAtPos.path)
90+
}
91+
onSelectionChanged(selectedFiles.size)
92+
}
93+
94+
val contentAlpha = if (isConflicted) 0.5f else 1.0f
95+
binding.filePath.alpha = contentAlpha
96+
binding.statusIcon.alpha = contentAlpha
97+
binding.root.alpha = 1.0f
6198

6299
val (imageRes, descRes) = when (change.type) {
63100
ChangeType.ADDED -> R.drawable.ic_file_added to R.string.desc_file_added

app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ import java.net.UnknownHostException
2121
import java.io.EOFException
2222
import java.io.File
2323
import com.blankj.utilcode.util.NetworkUtils
24+
import com.itsaky.androidide.git.core.GitCredentialsManager
2425
import com.itsaky.androidide.resources.R
2526

26-
class CloneRepositoryViewModel(application: Application) : AndroidViewModel(application) {
27+
class CloneRepositoryViewModel(
28+
application: Application,
29+
private val credentialsManager: GitCredentialsManager
30+
) : AndroidViewModel(application) {
2731

2832
private val _uiState = MutableStateFlow<CloneRepoUiState>(CloneRepoUiState.Idle())
2933
val uiState: StateFlow<CloneRepoUiState> = _uiState.asStateFlow()
@@ -182,6 +186,7 @@ class CloneRepositoryViewModel(application: Application) : AndroidViewModel(appl
182186
_uiState.update {
183187
CloneRepoUiState.Success(localPath = localPath)
184188
}
189+
credentialsManager.saveCredentialsIfNeeded(username, token)
185190
} catch (e: Exception) {
186191
// Error handling
187192
if (isCloneCancelled) {

app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.asStateFlow
2424
import kotlinx.coroutines.launch
2525
import org.eclipse.jgit.api.MergeResult.MergeStatus
2626
import org.eclipse.jgit.api.PullResult
27-
import org.eclipse.jgit.errors.NoRemoteRepositoryException
2827
import org.eclipse.jgit.api.errors.CheckoutConflictException
2928
import org.eclipse.jgit.transport.RemoteRefUpdate
3029
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
@@ -369,4 +368,17 @@ class GitBottomSheetViewModel(private val credentialsManager: GitCredentialsMana
369368
}
370369
}
371370

371+
fun resolveConflict(path: String) {
372+
viewModelScope.launch {
373+
try {
374+
val repository = currentRepository ?: return@launch
375+
val projectDir = File(IProjectManager.getInstance().projectDirPath)
376+
repository.stageFiles(listOf(File(projectDir, path)))
377+
refreshStatus()
378+
} catch (e: Exception) {
379+
log.error("Failed to resolve conflict for $path", e)
380+
}
381+
}
382+
}
383+
372384
}

app/src/main/res/layout/fragment_clone_repository.xml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,5 @@
160160
android:visibility="gone"
161161
app:layout_constraintTop_toBottomOf="@id/progressBar" />
162162

163-
<TextView
164-
android:id="@+id/statusText"
165-
android:layout_width="match_parent"
166-
android:layout_height="wrap_content"
167-
android:layout_marginTop="8dp"
168-
android:gravity="center"
169-
app:layout_constraintTop_toBottomOf="@id/progressText" />
170163
</androidx.constraintlayout.widget.ConstraintLayout>
171164
</ScrollView>

0 commit comments

Comments
 (0)