@@ -32,21 +32,21 @@ import com.itsaky.androidide.idetooltips.TooltipManager
3232import com.itsaky.androidide.idetooltips.TooltipTag.SETUP_CREATE_PROJECT
3333import com.itsaky.androidide.idetooltips.TooltipTag.SETUP_OVERVIEW
3434import com.itsaky.androidide.idetooltips.TooltipTag.SETUP_PREVIOUS
35- import com.itsaky.androidide.roomData.recentproject.RecentProject
36- import com.itsaky.androidide.tasks.executeAsyncProvideError
3735import com.itsaky.androidide.templates.ParameterWidget
38- import com.itsaky.androidide.templates.ProjectTemplateRecipeResult
39- import com.itsaky.androidide.templates.StringParameter
4036import com.itsaky.androidide.templates.Template
41- import com.itsaky.androidide.templates.impl.ConstraintVerifier
42- import com.itsaky.androidide.utils.TemplateRecipeExecutor
37+ import com.itsaky.androidide.utils.ProjectCreationManager
38+ import com.itsaky.androidide.utils.ui.TemplateScrollGateKeeper
4339import com.itsaky.androidide.utils.flashError
4440import com.itsaky.androidide.utils.flashSuccess
4541import com.itsaky.androidide.viewmodel.MainViewModel
4642import kotlinx.coroutines.Dispatchers
4743import kotlinx.coroutines.Job
4844import kotlinx.coroutines.launch
4945import kotlinx.coroutines.withContext
46+ import android.animation.ObjectAnimator
47+ import android.view.animation.LinearInterpolator
48+ import androidx.core.view.ViewCompat
49+ import androidx.core.view.isVisible
5050
5151/* *
5252 * A fragment which shows a wizard-like interface for creating templates.
@@ -61,24 +61,65 @@ class TemplateDetailsFragment :
6161 private val viewModel by activityViewModel<MainViewModel >()
6262 private var widgetsBindJob: Job ? = null
6363
64+ private var scrollGateKeeper: TemplateScrollGateKeeper ? = null
65+ private val projectCreationManager by lazy { ProjectCreationManager (requireContext()) }
66+ private var blinkAnimator: ObjectAnimator ? = null
67+
6468 override fun onViewCreated (view : View , savedInstanceState : Bundle ? ) {
6569 super .onViewCreated(view, savedInstanceState)
6670
71+ setupRecyclerView()
72+ setupTooltips()
73+ setupObservers()
74+ setupClickListeners()
75+ startBlinkingIndicator()
76+ }
77+
78+ override fun onDestroyView () {
79+ super .onDestroyView()
80+
81+ blinkAnimator?.cancel()
82+ blinkAnimator = null
83+
84+ scrollGateKeeper?.detach()
85+ scrollGateKeeper = null
86+ }
87+
88+ private fun setupRecyclerView () {
89+ binding.widgets.layoutManager = LinearLayoutManager (requireContext())
90+
91+ scrollGateKeeper = TemplateScrollGateKeeper (binding.widgets) {
92+ updateFinishEnabledState()
93+ }
94+ scrollGateKeeper?.attach()
95+ }
96+
97+ private fun setupObservers () {
6798 viewModel.template.observe(viewLifecycleOwner) {
6899 binding.widgets.adapter = null
100+ scrollGateKeeper?.reset()
101+ updateFinishEnabledState()
69102 viewModel.postTransition(viewLifecycleOwner) { bindWithTemplate(it) }
70103 }
71104
72- viewModel.creatingProject.observe(viewLifecycleOwner) {
105+ viewModel.creatingProject.observe(viewLifecycleOwner) { isCreating ->
73106 TransitionManager .beginDelayedTransition(binding.root)
74- binding.finish.isEnabled = ! it
75- binding.previous.isEnabled = ! it
107+ updateFinishEnabledState()
108+ binding.previous.isEnabled = ! isCreating
76109 }
110+ }
77111
112+ private fun setupClickListeners () {
78113 binding.previous.setOnClickListener {
79114 viewModel.setScreen(MainViewModel .SCREEN_TEMPLATE_LIST )
80115 }
81116
117+ binding.finish.setOnClickListener {
118+ handleProjectCreation()
119+ }
120+ }
121+
122+ private fun setupTooltips () {
82123 binding.previous.setOnLongClickListener {
83124 TooltipManager .showIdeCategoryTooltip(requireContext(), it, SETUP_PREVIOUS )
84125 true
@@ -89,59 +130,26 @@ class TemplateDetailsFragment :
89130 true
90131 }
91132
92- binding.finish.setOnClickListener {
93- viewModel.creatingProject.value = true
94- val template = viewModel.template.value ? : run {
95- viewModel.setScreen(MainViewModel .SCREEN_MAIN )
96- return @setOnClickListener
97- }
98-
99- val isValid = template.parameters.fold(true ) { isValid, param ->
100- if (param is StringParameter ) {
101- return @fold isValid && ConstraintVerifier .isValid(
102- param.value,
103- param.constraints
104- )
105- } else isValid
106- }
107-
108- if (! isValid) {
109- viewModel.creatingProject.value = false
110- flashError(string.msg_invalid_project_details)
111- return @setOnClickListener
112- }
133+ binding.title.setOnLongClickListener {
134+ TooltipManager .showIdeCategoryTooltip(requireContext(), binding.root, SETUP_OVERVIEW )
135+ true
136+ }
137+ }
113138
114- viewModel.creatingProject.value = true
115- val appContext = requireContext().applicationContext
116- executeAsyncProvideError({
117- template.recipe.execute( TemplateRecipeExecutor (appContext))
118- }) { result, err ->
139+ private fun handleProjectCreation () {
140+ val template = viewModel.template.value ? : run {
141+ viewModel.setScreen( MainViewModel . SCREEN_MAIN )
142+ return
143+ }
119144
145+ projectCreationManager.execute(
146+ template = template,
147+ onStart = { viewModel.creatingProject.value = true },
148+ onSuccess = { result, project ->
120149 viewModel.creatingProject.value = false
121- if (result == null || err != null || result !is ProjectTemplateRecipeResult ) {
122- err?.printStackTrace()
123- if (err != null ) {
124- flashError(err.cause?.message ? : err.message)
125- } else {
126- flashError(string.project_creation_failed)
127- }
128- return @executeAsyncProvideError
129- }
130-
131150 viewModel.setScreen(MainViewModel .SCREEN_MAIN )
132151 flashSuccess(string.project_created_successfully)
133152
134- val now = System .currentTimeMillis().toString()
135-
136- val project = RecentProject (
137- location = result.data.projectDir.path,
138- name = result.data.name,
139- createdAt = now,
140- lastModified = now,
141- templateName = template.templateNameStr,
142- language = result.data.language?.name ? : " unknown"
143- )
144-
145153 viewModel.postTransition(viewLifecycleOwner) {
146154 // open the project
147155 (requireActivity() as MainActivity ).openProject(
@@ -150,18 +158,12 @@ class TemplateDetailsFragment :
150158 hasTemplateIssues = result.hasErrorsWarnings
151159 )
152160 }
161+ },
162+ onError = { errorMsg ->
163+ viewModel.creatingProject.value = false
164+ flashError(errorMsg)
153165 }
154- }
155-
156- binding.widgets.layoutManager = LinearLayoutManager (requireContext())
157-
158- binding.title.setOnLongClickListener {
159- TooltipManager .showIdeCategoryTooltip(
160- requireContext(), binding.root,
161- SETUP_OVERVIEW
162- )
163- true
164- }
166+ )
165167 }
166168
167169 private fun bindWithTemplate (template : Template <* >? ) {
@@ -183,6 +185,29 @@ class TemplateDetailsFragment :
183185 }
184186 _binding ? : return @launch
185187 binding.widgets.adapter = TemplateWidgetsListAdapter (template.widgets)
188+ binding.widgets.post {
189+ scrollGateKeeper?.checkIfReachedEnd()
190+ }
191+ }
192+ }
193+
194+ private fun updateFinishEnabledState () {
195+ val isCreating = viewModel.creatingProject.value ? : false
196+ val hasScrolledToBottom = scrollGateKeeper?.hasReachedEnd ? : false
197+ val canFinish = ! isCreating && hasScrolledToBottom
198+ val stateDesc = if (canFinish) null else getString(string.msg_scroll_to_create_project)
199+
200+ binding.finish.isEnabled = ! isCreating && hasScrolledToBottom
201+ binding.scrollIndicator.isVisible = ! hasScrolledToBottom
202+ ViewCompat .setStateDescription(binding.finish, stateDesc)
203+ }
204+
205+ private fun startBlinkingIndicator () {
206+ blinkAnimator = ObjectAnimator .ofFloat(binding.scrollIndicator, View .ALPHA , 1f , 0.2f , 1f ).apply {
207+ duration = 1200
208+ interpolator = LinearInterpolator ()
209+ repeatCount = ObjectAnimator .INFINITE
210+ start()
186211 }
187212 }
188- }
213+ }
0 commit comments