feat: implement UI for new templates API

This commit is contained in:
Akash Yadav
2023-05-21 18:36:55 +05:30
parent 86907ff5e9
commit 84e2ed5f9b
36 changed files with 1568 additions and 158 deletions
+7
View File
@@ -6,6 +6,7 @@ plugins {
id("kotlin-kapt")
id("kotlin-parcelize")
id("com.google.android.gms.oss-licenses-plugin")
id("androidx.navigation.safeargs.kotlin")
}
android {
@@ -101,11 +102,15 @@ dependencies {
implementation(libs.androidx.coordinatorlayout)
implementation(libs.androidx.drawer)
implementation(libs.androidx.grid)
implementation(libs.androidx.nav.fragment)
implementation(libs.androidx.nav.ui)
implementation(libs.androidx.preference)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.transition)
implementation(libs.androidx.vectors)
implementation(libs.androidx.animated.vectors)
implementation(libs.google.material)
implementation(libs.google.flexbox)
implementation(libs.google.oss.licenses)
// Kotlin
@@ -136,6 +141,8 @@ dependencies {
implementation(projects.preferences)
implementation(projects.resources)
implementation(projects.treeview)
implementation(projects.templatesApi)
implementation(projects.templatesImpl)
implementation(projects.uidesigner)
implementation(projects.xmlInflater)
+2 -1
View File
@@ -23,6 +23,7 @@
android:name=".app.IDEApplication"
android:allowBackup="false"
android:allowNativeHeapPointerTagging="false"
android:enableOnBackInvokedCallback="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -76,7 +77,7 @@
android:exported="true"
android:permission="com.itsaky.androidide.permission.BIND_LOG_SERVICE">
<intent-filter>
<action android:name="com.itsaky.androidide.LOG_SERVICE_ACTION"/>
<action android:name="com.itsaky.androidide.LOG_SERVICE_ACTION" />
</intent-filter>
</service>
Binary file not shown.
@@ -16,23 +16,26 @@
*/
package com.itsaky.androidide.activities
import android.R
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import androidx.transition.doOnEnd
import com.blankj.utilcode.util.SizeUtils
import com.google.android.material.color.DynamicColors
import com.itsaky.androidide.R.id
import com.google.android.material.transition.MaterialSharedAxis
import com.itsaky.androidide.activities.editor.EditorActivityKt
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.app.IDEActivity
import com.itsaky.androidide.databinding.ActivityMainBinding
import com.itsaky.androidide.fragments.MainFragment
import com.itsaky.androidide.preferences.internal.NO_OPENED_PROJECT
import com.itsaky.androidide.preferences.internal.autoOpenProjects
import com.itsaky.androidide.preferences.internal.confirmProjectOpen
@@ -43,28 +46,106 @@ import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashInfo
import com.itsaky.androidide.viewmodel.MainViewModel
import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_MAIN
import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_TEMPLATE_DETAILS
import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_TEMPLATE_LIST
import java.io.File
class MainActivity : IDEActivity() {
private var binding: ActivityMainBinding? = null
private val viewModel by viewModels<MainViewModel>()
private var _binding: ActivityMainBinding? = null
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
viewModel.apply {
val newScreen = when (currentScreen.value) {
SCREEN_TEMPLATE_DETAILS -> SCREEN_TEMPLATE_LIST
SCREEN_TEMPLATE_LIST -> SCREEN_MAIN
else -> SCREEN_MAIN
}
if (currentScreen.value != newScreen) {
setScreen(newScreen)
}
}
}
}
private val binding: ActivityMainBinding
get() = checkNotNull(_binding)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!BaseApplication.isAbiSupported()) {
showDeviceNotSupported()
return
}
if (!checkToolsIsInstalled()) {
showDialogInstallJdkSdk()
} else {
openLastProject()
}
supportFragmentManager.beginTransaction()
.replace(id.container, MainFragment(), MainFragment.TAG)
.commit()
viewModel.currentScreen.observe(this) { screen ->
if (screen == -1) {
return@observe
}
try {
onScreenChanged(screen)
} catch (err: Exception) {
onBackPressedCallback.isEnabled = screen != SCREEN_MAIN
}
}
viewModel.setScreen(SCREEN_MAIN)
onBackPressedDispatcher.addCallback(/* owner = */
this, /* onBackPressedCallback = */ onBackPressedCallback)
}
private fun onScreenChanged(screen: Int?) {
val previous = viewModel.previousScreen
if (previous != -1) {
val axis =
if ((previous == SCREEN_TEMPLATE_LIST || previous == SCREEN_TEMPLATE_DETAILS) && (screen == SCREEN_TEMPLATE_LIST || screen == SCREEN_TEMPLATE_DETAILS)) {
MaterialSharedAxis.X
} else MaterialSharedAxis.Y
val isForward = when {
previous == SCREEN_MAIN && screen == SCREEN_TEMPLATE_LIST -> true
previous == SCREEN_TEMPLATE_LIST && screen == SCREEN_TEMPLATE_DETAILS -> true
previous == SCREEN_TEMPLATE_DETAILS && screen == SCREEN_TEMPLATE_LIST -> false
previous == SCREEN_TEMPLATE_LIST && screen == SCREEN_MAIN -> false
else -> throw IllegalStateException("Invalid screen states")
}
val transition = MaterialSharedAxis(axis, isForward)
transition.doOnEnd {
onBackPressedCallback.isEnabled =
viewModel.currentScreen.value != SCREEN_MAIN
}
TransitionManager.beginDelayedTransition(binding.root, transition)
}
val view = when (screen) {
SCREEN_MAIN -> binding.main
SCREEN_TEMPLATE_LIST -> binding.templateList
SCREEN_TEMPLATE_DETAILS -> binding.templateDetails
else -> throw IllegalArgumentException("Invalid screen id: '$screen'")
}
for (fragment in arrayOf(binding.main, binding.templateList,
binding.templateDetails)) {
fragment.isVisible = fragment == view
}
}
override fun onStorageGranted() {}
override fun onStorageDenied() {
flashError(string.msg_storage_denied)
finishAffinity()
@@ -78,8 +159,8 @@ class MainActivity : IDEActivity() {
}
override fun bindLayout(): View {
binding = ActivityMainBinding.inflate(layoutInflater)
return binding!!.root
_binding = ActivityMainBinding.inflate(layoutInflater)
return binding.root
}
private fun showDialogInstallJdkSdk() {
@@ -94,8 +175,9 @@ class MainActivity : IDEActivity() {
view.movementMethod = LinkMovementMethod.getInstance()
builder.setView(view)
builder.setCancelable(false)
builder.setPositiveButton(R.string.ok) { _, _ -> openTerminal() }
builder.setNegativeButton(R.string.cancel) { _, _ -> finishAffinity() }
builder.setPositiveButton(android.R.string.ok) { _, _ -> openTerminal() }
builder.setNegativeButton(
android.R.string.cancel) { _, _ -> finishAffinity() }
builder.setNeutralButton(string.btn_docs) { _, _ -> app.openDocs() }
builder.show()
}
@@ -109,36 +191,41 @@ class MainActivity : IDEActivity() {
builder.setTitle(string.title_device_not_supported)
builder.setMessage(string.msg_device_not_supported)
builder.setCancelable(false)
builder.setPositiveButton(R.string.ok) { _, _ -> finishAffinity() }
builder.setPositiveButton(android.R.string.ok) { _, _ -> finishAffinity() }
builder.create().show()
}
private fun openLastProject() {
binding!!.root.post { tryOpenLastProject() }
binding.root.post { tryOpenLastProject() }
}
private fun tryOpenLastProject() {
if (!autoOpenProjects) {
return
}
val openedProject = lastOpenedProject
if (NO_OPENED_PROJECT == openedProject) {
return
}
if (TextUtils.isEmpty(openedProject)) {
app
flashInfo(string.msg_opened_project_does_not_exist)
return
}
val project = File(openedProject)
if (!project.exists()) {
flashInfo(string.msg_opened_project_does_not_exist)
return
}
if (confirmProjectOpen) {
askProjectOpenPermission(project)
return
}
openProject(project)
}
@@ -164,6 +251,6 @@ class MainActivity : IDEActivity() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}
@@ -0,0 +1,104 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.blankj.utilcode.util.ConvertUtils
import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.material.shape.CornerFamily
import com.itsaky.androidide.adapters.TemplateListAdapter.ViewHolder
import com.itsaky.androidide.databinding.LayoutTemplateListItemBinding
import com.itsaky.androidide.templates.Template
/**
* [RecyclerView.Adapter] for showing templates in a [RecyclerView].
*
* @author Akash Yadav
*/
class TemplateListAdapter(templates: List<Template>, private val onClick: ((Template, ViewHolder) -> Unit)? = null
) : RecyclerView.Adapter<ViewHolder>() {
private val templates = templates.toMutableList()
class ViewHolder(internal val binding: LayoutTemplateListItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int
): ViewHolder {
return ViewHolder(
LayoutTemplateListItemBinding.inflate(LayoutInflater.from(parent.context),
parent, false))
}
override fun getItemCount(): Int {
return templates.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.apply {
val template = templates[position]
if (template == Template.EMPTY) {
root.visibility = View.INVISIBLE
return@apply
}
templateName.setText(template.templateName)
templateIcon.setImageResource(template.thumb)
templateIcon.shapeAppearanceModel =
templateIcon.shapeAppearanceModel.toBuilder()
.setAllCorners(CornerFamily.ROUNDED, ConvertUtils.dp2px(8f).toFloat())
.build()
root.setOnClickListener {
onClick?.invoke(template, holder)
}
}
}
internal fun fillDiff(extras: Int) {
val count = itemCount
for (i in 1..extras) {
templates.add(Template.EMPTY)
}
val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return count
}
override fun getNewListSize(): Int {
return count + extras
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int
): Boolean {
return newItemPosition < count && oldItemPosition == newItemPosition
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int
): Boolean {
return areItemsTheSame(oldItemPosition, newItemPosition)
}
})
diff.dispatchUpdatesTo(this)
}
}
@@ -0,0 +1,63 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.adapters.TemplateWidgetsListAdapter.WidgetViewHolder
import com.itsaky.androidide.databinding.LayoutTemplateWidgetlistItemBinding
import com.itsaky.androidide.templates.ITemplateWidgetViewProvider
import com.itsaky.androidide.templates.Widget
/**
* A [RecyclerView.Adapter] that is used to show the widgets from templates.
*
* @author Akash Yadav
*/
class TemplateWidgetsListAdapter(private val widgets: List<Widget<*>>) :
RecyclerView.Adapter<WidgetViewHolder>() {
class WidgetViewHolder(
internal val binding: LayoutTemplateWidgetlistItemBinding
) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int
): WidgetViewHolder {
return WidgetViewHolder(LayoutTemplateWidgetlistItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false))
}
override fun getItemCount(): Int {
return widgets.size
}
override fun onBindViewHolder(holder: WidgetViewHolder, position: Int) {
holder.binding.apply {
val viewProvider = ITemplateWidgetViewProvider.getInstance()
val widget = widgets[position]
val view = viewProvider.createView(root.context, widget)
root.removeAllViews()
root.addView(view,
LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT))
}
}
}
@@ -3,17 +3,14 @@ package com.itsaky.androidide.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.blankj.utilcode.util.ConvertUtils;
import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.shape.CornerFamily;
import com.itsaky.androidide.databinding.WizardTemplateItemBinding;
import com.itsaky.androidide.databinding.LayoutTemplateListItemBinding;
import com.itsaky.androidide.models.ProjectTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -23,36 +20,35 @@ public class WizardTemplateAdapter extends RecyclerView.Adapter<WizardTemplateAd
private final List<ProjectTemplate> mItems = new ArrayList<>();
private OnItemClickListener mListener;
public WizardTemplateAdapter() {}
public WizardTemplateAdapter() {
}
public void setOnItemClickListener(OnItemClickListener listener) {
mListener = listener;
}
public void submitList(@NonNull List<ProjectTemplate> newItems) {
DiffUtil.DiffResult diffResult =
DiffUtil.calculateDiff(
new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return mItems.size();
}
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return mItems.size();
}
@Override
public int getNewListSize() {
return newItems.size();
}
@Override
public int getNewListSize() {
return newItems.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return Objects.equals(mItems.get(oldItemPosition), newItems.get(newItemPosition));
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return Objects.equals(mItems.get(oldItemPosition), newItems.get(newItemPosition));
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return Objects.equals(mItems.get(oldItemPosition), newItems.get(newItemPosition));
}
});
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return Objects.equals(mItems.get(oldItemPosition), newItems.get(newItemPosition));
}
});
mItems.clear();
mItems.addAll(newItems);
diffResult.dispatchUpdatesTo(this);
@@ -61,21 +57,18 @@ public class WizardTemplateAdapter extends RecyclerView.Adapter<WizardTemplateAd
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
WizardTemplateItemBinding binding =
WizardTemplateItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
LayoutTemplateListItemBinding binding = LayoutTemplateListItemBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
ViewHolder holder = new ViewHolder(binding);
binding
.getRoot()
.setOnClickListener(
view1 -> {
if (mListener != null) {
int pos = holder.getBindingAdapterPosition();
if (pos != RecyclerView.NO_POSITION) {
mListener.onItemClick(mItems.get(pos), pos);
}
}
});
binding.getRoot().setOnClickListener(view1 -> {
if (mListener != null) {
int pos = holder.getBindingAdapterPosition();
if (pos != RecyclerView.NO_POSITION) {
mListener.onItemClick(mItems.get(pos), pos);
}
}
});
return holder;
}
@@ -94,7 +87,7 @@ public class WizardTemplateAdapter extends RecyclerView.Adapter<WizardTemplateAd
public final ShapeableImageView icon;
public final TextView name;
public ViewHolder(WizardTemplateItemBinding view) {
public ViewHolder(LayoutTemplateListItemBinding view) {
super(view.getRoot());
icon = view.templateIcon;
@@ -104,14 +97,15 @@ public class WizardTemplateAdapter extends RecyclerView.Adapter<WizardTemplateAd
private void bind(ProjectTemplate template) {
name.setText(template.getName());
icon.setImageResource(template.getImageId());
icon.setShapeAppearanceModel(
icon.getShapeAppearanceModel().toBuilder()
.setAllCorners(CornerFamily.ROUNDED, ConvertUtils.dp2px(8))
.build());
icon.setShapeAppearanceModel(icon.getShapeAppearanceModel()
.toBuilder()
.setAllCorners(CornerFamily.ROUNDED, ConvertUtils.dp2px(8))
.build());
}
}
public interface OnItemClickListener {
void onItemClick(ProjectTemplate item, int position);
}
}
@@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.viewModels
import com.blankj.utilcode.util.ThreadUtils
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.itsaky.androidide.activities.PreferencesActivity
@@ -27,19 +28,21 @@ import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashSuccess
import com.itsaky.androidide.viewmodel.MainViewModel
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ProgressMonitor
import java.io.File
class MainFragment : BaseFragment(), OnProjectCreatedListener {
private val viewModel by viewModels<MainViewModel>(
ownerProducer = { requireActivity() })
private var binding: FragmentMainBinding? = null
private val log = ILogger.newInstance("MainFragment")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentMainBinding.inflate(inflater, container, false)
return binding!!.root
@@ -47,39 +50,28 @@ class MainFragment : BaseFragment(), OnProjectCreatedListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val createProject =
MainScreenAction(string.create_project, R.drawable.ic_add) { showCreateProject() }
val openProject =
MainScreenAction(string.msg_open_existing_project, R.drawable.ic_folder) { pickDirectory() }
val cloneGitRepository =
MainScreenAction(string.git_clone_repo, R.drawable.ic_git) { cloneGitRepo() }
val createProject = MainScreenAction(string.create_project,
R.drawable.ic_add) { showCreateProject() }
val openProject = MainScreenAction(string.msg_open_existing_project,
R.drawable.ic_folder) { pickDirectory() }
val cloneGitRepository = MainScreenAction(string.git_clone_repo,
R.drawable.ic_git) { cloneGitRepo() }
val openTerminal =
MainScreenAction(string.title_terminal, R.drawable.ic_terminal) {
startActivity(Intent(requireActivity(), TerminalActivity::class.java))
}
val preferences =
MainScreenAction(string.msg_preferences, R.drawable.ic_settings) { gotoPreferences() }
val sponsor =
MainScreenAction(string.btn_donate, R.drawable.ic_donate) {
BaseApplication.getBaseInstance().openSponsors()
}
val docs =
MainScreenAction(string.btn_docs, R.drawable.ic_docs) {
BaseApplication.getBaseInstance().openDocs()
}
val preferences = MainScreenAction(string.msg_preferences,
R.drawable.ic_settings) { gotoPreferences() }
val sponsor = MainScreenAction(string.btn_donate, R.drawable.ic_donate) {
BaseApplication.getBaseInstance().openSponsors()
}
val docs = MainScreenAction(string.btn_docs, R.drawable.ic_docs) {
BaseApplication.getBaseInstance().openDocs()
}
binding!!.actions.adapter =
MainActionsListAdapter(
listOf(
createProject,
openProject,
cloneGitRepository,
openTerminal,
preferences,
docs,
sponsor
)
)
binding!!.actions.adapter = MainActionsListAdapter(
listOf(createProject, openProject, cloneGitRepository, openTerminal,
preferences, docs, sponsor))
}
override fun onDestroyView() {
@@ -92,13 +84,7 @@ class MainFragment : BaseFragment(), OnProjectCreatedListener {
}
private fun showCreateProject() {
val wizardFragment = WizardFragment()
wizardFragment.setOnProjectCreatedListener(this)
parentFragmentManager
.beginTransaction()
.add(com.itsaky.androidide.R.id.container, wizardFragment, WizardFragment.TAG)
.addToBackStack(null)
.commit()
viewModel.setScreen(MainViewModel.SCREEN_TEMPLATE_LIST)
}
override fun openProject(root: File) {
@@ -149,18 +135,14 @@ class MainFragment : BaseFragment(), OnProjectCreatedListener {
val progress = GitCloneProgressMonitor(binding.progress, binding.message)
var git: Git? = null
val future =
executeAsyncProvideError(
{
return@executeAsyncProvideError Git.cloneRepository()
.setURI(url)
.setDirectory(targetDir)
.setProgressMonitor(progress)
.call()
.also { git = it }
},
{ _, _ -> }
)
val future = executeAsyncProvideError({
return@executeAsyncProvideError Git.cloneRepository()
.setURI(url)
.setDirectory(targetDir)
.setProgressMonitor(progress)
.call()
.also { git = it }
}, { _, _ -> })
builder.setPositiveButton(android.R.string.cancel) { iface, _ ->
iface.dismiss()
@@ -201,13 +183,10 @@ class MainFragment : BaseFragment(), OnProjectCreatedListener {
startActivity(Intent(requireActivity(), PreferencesActivity::class.java))
}
companion object {
const val TAG = "MainFragmentTag"
}
// TODO(itsaky) : Improve this implementation
class GitCloneProgressMonitor(val progress: LinearProgressIndicator, val message: TextView) :
ProgressMonitor {
class GitCloneProgressMonitor(val progress: LinearProgressIndicator,
val message: TextView
) : ProgressMonitor {
private var cancelled = false
@@ -0,0 +1,70 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments
import android.os.Bundle
import android.view.View
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.TemplateWidgetsListAdapter
import com.itsaky.androidide.databinding.FragmentTemplateDetailsBinding
import com.itsaky.androidide.templates.Template
import com.itsaky.androidide.viewmodel.MainViewModel
/**
* A fragment which shows a wizard-like interface for creating templates.
*
* @author Akash Yadav
*/
class TemplateDetailsFragment :
FragmentWithBinding<FragmentTemplateDetailsBinding>(
R.layout.fragment_template_details, FragmentTemplateDetailsBinding::bind) {
private val viewModel by viewModels<MainViewModel>(
ownerProducer = { requireActivity() })
private val template: Template
get() = checkNotNull(
viewModel.template.value) { "Invalid template data stored in view model" }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.template.observe(viewLifecycleOwner) {
it ?: return@observe
binding.widgets.layoutManager = LinearLayoutManager(requireContext())
binding.widgets.adapter = TemplateWidgetsListAdapter(template.widgets)
binding.title.setText(template.templateName)
binding.previous.setOnClickListener {
viewModel.setScreen(MainViewModel.SCREEN_TEMPLATE_LIST)
}
binding.widgets.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.widgets.viewTreeObserver.removeOnGlobalLayoutListener(this)
startPostponedEnterTransition()
}
})
}
}
}
@@ -0,0 +1,100 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments
import android.os.Bundle
import android.view.View
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import androidx.fragment.app.viewModels
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.flexbox.JustifyContent
import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.TemplateListAdapter
import com.itsaky.androidide.databinding.FragmentTemplateListBinding
import com.itsaky.androidide.templates.ITemplateProvider
import com.itsaky.androidide.viewmodel.MainViewModel
import kotlin.math.ceil
/**
* A fragment to show the list of available templates.
*
* @author Akash Yadav
*/
class TemplateListFragment : FragmentWithBinding<FragmentTemplateListBinding>(
R.layout.fragment_template_list, FragmentTemplateListBinding::bind) {
private var adapter: TemplateListAdapter? = null
private var layoutManager: FlexboxLayoutManager? = null
private val viewModel by viewModels<MainViewModel>(
ownerProducer = { requireActivity() })
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val templates = ITemplateProvider.getInstance().getTemplates()
layoutManager =
FlexboxLayoutManager(requireContext(), FlexDirection.ROW).apply {
justifyContent = JustifyContent.SPACE_EVENLY
}
adapter = TemplateListAdapter(templates) { template, _ ->
viewModel.template.value = template
viewModel.setScreen(MainViewModel.SCREEN_TEMPLATE_DETAILS)
}
binding.list.layoutManager = layoutManager
binding.list.adapter = adapter
// This makes sure that the items are evenly distributed in the list
// and the last row is always aligned to the start
binding.list.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.list.viewTreeObserver.removeOnGlobalLayoutListener(this)
val adapter = this@TemplateListFragment.adapter ?: return
val layoutManager = this@TemplateListFragment.layoutManager ?: return
val columns =
layoutManager.flexLinesInternal.firstOrNull()?.itemCount ?: 0
if (columns == 0) {
return
}
val itemCount = adapter.itemCount
val rows = ceil(itemCount.toFloat() / columns.toFloat()).toInt()
if (itemCount % columns == 0) {
return
}
val diff = rows * columns - itemCount
if (diff <= 0) {
return
}
adapter.fillDiff(diff)
}
})
binding.exitButton.setOnClickListener {
viewModel.setScreen(MainViewModel.SCREEN_MAIN)
}
}
}
@@ -0,0 +1,54 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.itsaky.androidide.templates.Template
import java.util.concurrent.atomic.AtomicInteger
/**
* [ViewModel] for main activity.
*
* @author Akash Yadav
*/
class MainViewModel : ViewModel() {
companion object {
// main screens
const val SCREEN_MAIN = 0
const val SCREEN_TEMPLATE_LIST = 1
const val SCREEN_TEMPLATE_DETAILS = 2
}
private val _currentScreen = MutableLiveData(-1)
private val _previousScreen = AtomicInteger(-1)
internal val template = MutableLiveData<Template>(null)
val currentScreen: LiveData<Int> = _currentScreen
val previousScreen: Int
get() = _previousScreen.get()
fun setScreen(screen: Int) {
_previousScreen.set(_currentScreen.value ?: SCREEN_MAIN)
_currentScreen.value = screen
}
}
+23 -3
View File
@@ -36,13 +36,33 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:id="@+id/main"
android:name="com.itsaky.androidide.fragments.MainFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:name="com.itsaky.androidide.fragments.MainFragment" />
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/template_list"
android:name="com.itsaky.androidide.fragments.TemplateListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/template_details"
android:name="com.itsaky.androidide.fragments.TemplateDetailsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout 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">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:transitionName="templateName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/new_project" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/widgets"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@id/previous"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<com.google.android.material.button.MaterialButton
android:id="@+id/previous"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/previous"
android:transitionName="negativeButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/finish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/create_project"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout 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">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/new_project"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:transitionName="title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@id/exit_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
tools:itemCount="10"
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/layout_template_list_item"
tools:spanCount="3" />
<com.google.android.material.button.MaterialButton
android:id="@+id/exit_button"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/exit"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/template_list"
android:name="com.itsaky.androidide.fragments.TemplateListFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/template_details"
android:name="com.itsaky.androidide.fragments.TemplateDetailsFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -13,12 +13,13 @@
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:gravity="center"
android:paddingBottom="8dp">
<com.google.android.material.imageview.ShapeableImageView
@@ -36,7 +37,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:maxLines="2"
android:lines="2"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
tools:text="Empty Activity" />
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:orientation="vertical" />
+6 -1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">
@@ -8,5 +9,9 @@
android:id="@+id/template_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/wizard_template_item" />
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:itemCount="10"
tools:listitem="@layout/layout_template_list_item"
tools:spanCount="3" />
</FrameLayout>
+1
View File
@@ -41,6 +41,7 @@ buildscript {
dependencies {
classpath("com.google.android.gms:oss-licenses-plugin:0.10.6")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3")
}
}
@@ -34,10 +34,11 @@ import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.FileUtil;
import com.itsaky.androidide.utils.FlashbarUtilsKt;
import com.itsaky.androidide.utils.JavaCharacter;
import com.itsaky.androidide.utils.VMUtils;
import java.io.File;
import java.util.Arrays;
public abstract class BaseApplication extends Application {
public class BaseApplication extends Application {
public static final String NOTIFICATION_GRADLE_BUILD_SERVICE = "17571";
public static final String TELEGRAM_GROUP_URL = "https://t.me/androidide_discussions";
@@ -85,7 +86,10 @@ public abstract class BaseApplication extends Application {
mPrefsManager = new PreferenceManager(this);
JavaCharacter.initMap();
ToolsManager.init(this, null);
if (!VMUtils.isJvm()) {
ToolsManager.init(this, null);
}
createNotificationChannels();
}
@@ -0,0 +1,54 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.viewbinding.ViewBinding
/**
* Handles inflation and release of [ViewBinding][androidx.viewbinding.ViewBinding] for the fragment.
*
* @author Akash Yadav
*/
abstract class FragmentWithBinding<T : ViewBinding>(@LayoutRes layout: Int,
private val bind: (View) -> T
) : BaseFragment(layout) {
@Suppress("PropertyName")
protected var _binding: T? = null
protected val binding: T
get() = checkNotNull(
_binding) { "Cannot access ViewBinding. Fragment may have been destroyed." }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return super.onCreateView(inflater, container, savedInstanceState)!!
.also { _binding = bind(it) }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
+5 -3
View File
@@ -57,9 +57,10 @@ androidx-ktx = { module = "androidx.core:core-ktx", version = "1.10.0" }
androidx-fragment_ktx = { module = "androidx.fragment:fragment-ktx", version = "1.5.7" }
androidx-lib_desugaring = { module = "com.android.tools:desugar_jdk_libs", version = "1.1.6" }
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" }
androidx-nav_frag = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" }
androidx-nav_ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" }
androidx-nav_dynamic_features = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "androidx-navigation" }
androidx-transition = { module = "androidx.transition:transition-ktx", version = "1.4.1" }
androidx-nav-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" }
androidx-nav-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" }
androidx-nav-dynamic_features = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "androidx-navigation" }
# Google
google-material = { module = "com.google.android.material:material", version = "1.8.0" }
@@ -73,6 +74,7 @@ google-auto-service = { module = "com.google.auto.service:auto-service", version
google-oss-licenses = { module = "com.google.android.gms:play-services-oss-licenses", version = "17.0.0" }
google-protobuf = { module = "com.google.protobuf:protobuf-java", version = "3.22.3" }
google-java-format = { module ="com.google.googlejavaformat:google-java-format", version="1.16.0" }
google-flexbox = { module = "com.google.android.flexbox:flexbox", version = "3.0.0" }
# AAPT2
aapt2-common = { module = "com.android.tools:common", version = "30.2.2" }
+8 -1
View File
@@ -553,5 +553,12 @@
<string name="permdesc_bind_logger_service">Allows this application to bind to AndroidIDE\'s logging service. This permission is required for AndroidIDE to read logs of this application.</string>
<string name="wizard_module_name">Module name</string>
<string name="wizard_module_type">Module type</string>
<string name="msg_invalid_module_name">Invalid Gradle module name</string>
<string name="msg_invalid_class_name">Invalid class name</string>
<string name="msg_value_empty">Value is empty</string>
<string name="msg_file_not_exist">File does not exist</string>
<string name="msg_path_must_be_file">Must be path to a file</string>
<string name="msg_classname_with_keywords">Class name contains keywords</string>
<string name="msg_path_must_be_dir">Must be path to a directory</string>
<string name="msg_invalid_layout_name">Invalid layout file name</string>
</resources>
@@ -0,0 +1,56 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.templates
import android.content.Context
import android.view.View
import com.itsaky.androidide.utils.ServiceLoader
/**
* [ITemplateWidgetViewProvider] creates [views][View] for various type of [widgets][Widget].
*
* @author Akash Yadav
*/
interface ITemplateWidgetViewProvider {
companion object {
private var service: ITemplateWidgetViewProvider? = null
@JvmStatic
@JvmOverloads
fun getInstance(reload: Boolean = false): ITemplateWidgetViewProvider {
if (reload) {
service = null
}
return service ?: ServiceLoader.load(
ITemplateWidgetViewProvider::class.java)
.findFirstOrThrow()
.also { service = it }
}
}
/**
* Creates [View] objects for the given widget.
*
* @param context The context used to create the views.
* @param widget The widget to create the view for.
* @return The view.
*/
fun createView(context: Context, widget: Widget<*>): View
}
@@ -64,6 +64,7 @@ fun baseProject(block: ProjectTemplateConfigurator): ProjectTemplate {
val projectName = stringParameter {
name = R.string.project_app_name
default = "My Application"
startIcon = R.drawable.ic_android
constraints = listOf(NONEMPTY)
}
@@ -36,6 +36,15 @@ internal fun ProjectTemplateData.moduleNameToDir(name: String): File {
return File(this.projectDir, moduleNameToDirName(name).replace(':', '/').trim { it == '/' })
}
fun isValidModuleName(name: String): Boolean {
if (name.length < 2 || name.isBlank() || name[0] != ':') {
return false
}
val moduleName = name.substring(1)
return moduleNameToDirName(moduleName) == moduleName
}
/**
* Converts a Gradle module name to a directory name.
*
@@ -56,8 +65,8 @@ fun moduleNameToDirName(name: String): String {
// the first character must be a letter
(i == 0 && !c.isLetter())
// chars at other indices must be a letter or digit or
|| !(c.isDigit() || c.isLetter() || c == '-')
// chars at other indices must be a letter, digit, hyphen or an underscore
|| !(c.isDigit() || c.isLetter() || c == '-' || c =='_')
// must not include consecutive '-'
|| (prev == '-' && c == '-')
@@ -74,8 +74,9 @@ enum class ParameterConstraint {
typealias ValueSuggestion<T> = Parameter<T>.() -> T?
abstract class Parameter<T>(@StringRes val name: Int, @StringRes val description: Int?,
val default: T, var constraints: List<ParameterConstraint>,
abstract class Parameter<T>(@StringRes val name: Int,
@StringRes val description: Int?, val default: T,
var constraints: List<ParameterConstraint>,
var suggest: ValueSuggestion<T>
) {
@@ -104,39 +105,83 @@ abstract class ParameterBuilder<T>() {
abstract fun build(): Parameter<T>
}
class StringParameter(@StringRes name: Int, @StringRes description: Int?, default: String,
constraints: List<ParameterConstraint>, suggest: ValueSuggestion<String>
) : Parameter<String>(name, description, default, constraints, suggest)
class StringParameterBuilder : ParameterBuilder<String>() {
override fun build(): StringParameter {
return StringParameter(name!!, description, default!!, constraints, suggest)
}
}
class BooleanParameter(@StringRes name: Int, @StringRes description: Int?, default: Boolean,
constraints: List<ParameterConstraint>, suggest: ValueSuggestion<Boolean>
class BooleanParameter(@StringRes name: Int, @StringRes description: Int?,
default: Boolean, constraints: List<ParameterConstraint>,
suggest: ValueSuggestion<Boolean>
) : Parameter<Boolean>(name, description, default, constraints, suggest)
class BooleanParameterBuilder : ParameterBuilder<Boolean>() {
override fun build(): BooleanParameter {
return BooleanParameter(name!!, description, default!!, constraints, suggest)
return BooleanParameter(name!!, description, default!!, constraints,
suggest)
}
}
/**
* Base class for parameters which are presented using a text field.
*
* @property startIcon The drawable resource that will be used as the start icon.
* @property endIcon The drawable resource that will be used as the end icon.
* @property onStartIconClick Function which will be used when the start icon
* is clicked. Click listener to the icon will be set on if this is non-null.
* @property onEndIconClick Function which will be used when the end icon is
* clicked. Click listener to the icon will be set on if this is non-null.
*/
abstract class TextFieldParameter<T>(@StringRes name: Int,
@StringRes description: Int?, default: T,
val startIcon: Int?, val endIcon: Int?,
val onStartIconClick: (() -> Unit)?,
val onEndIconClick: (() -> Unit)?,
constraints: List<ParameterConstraint>,
suggest: ValueSuggestion<T>
) : Parameter<T>(name, description, default, constraints, suggest)
abstract class TextFieldParameterBuilder<T>(var startIcon: Int? = null,
var endIcon: Int? = null,
var onStartIconClick: (() -> Unit)? = null,
var onEndIconClick: (() -> Unit)? = null
) : ParameterBuilder<T>()
class StringParameter(@StringRes name: Int, @StringRes description: Int?,
default: String, startIcon: Int?, endIcon: Int?,
onStartIconClick: (() -> Unit)?,
onEndIconClick: (() -> Unit)?,
constraints: List<ParameterConstraint>,
suggest: ValueSuggestion<String>
) : TextFieldParameter<String>(name, description, default, startIcon, endIcon,
onStartIconClick, onEndIconClick, constraints, suggest)
class StringParameterBuilder() : TextFieldParameterBuilder<String>() {
override fun build(): StringParameter {
return StringParameter(name = name!!, description = description,
default = default!!, startIcon = startIcon, endIcon = endIcon,
onStartIconClick = onStartIconClick, onEndIconClick = onEndIconClick,
constraints = constraints, suggest = suggest)
}
}
class EnumParameter<T : Enum<*>>(@StringRes name: Int, @StringRes description: Int?, default: T,
class EnumParameter<T : Enum<*>>(@StringRes name: Int,
@StringRes description: Int?, default: T,
startIcon: Int?, endIcon: Int?,
onStartIconClick: (() -> Unit)?,
onEndIconClick: (() -> Unit)?,
constraints: List<ParameterConstraint>,
suggest: ValueSuggestion<T>, val displayName: ((T) -> String)? = null
) : Parameter<T>(name, description, default, constraints, suggest)
suggest: ValueSuggestion<T>,
val displayName: ((T) -> String)? = null
) : TextFieldParameter<T>(name, description, default, startIcon, endIcon,
onStartIconClick, onEndIconClick, constraints, suggest)
class EnumParameterBuilder<T : Enum<*>> : ParameterBuilder<T>() {
class EnumParameterBuilder<T : Enum<*>> : TextFieldParameterBuilder<T>() {
var displayName: ((T) -> String)? = null
override fun build(): EnumParameter<T> {
return EnumParameter(name = name!!, description = description, default = default!!,
return EnumParameter(name = name!!, description = description,
default = default!!, startIcon = startIcon, endIcon = endIcon,
onStartIconClick = onStartIconClick, onEndIconClick = onEndIconClick,
constraints = constraints, suggest = suggest, displayName = displayName)
}
}
@@ -153,5 +198,5 @@ fun stringParameter(block: StringParameterBuilder.() -> Unit
fun booleanParameter(block: BooleanParameterBuilder.() -> Unit
): BooleanParameter = BooleanParameterBuilder().apply(block).build()
fun <T : Enum<*>> enumParameter(block: EnumParameterBuilder<T>.() -> Unit): EnumParameter<T> =
EnumParameterBuilder<T>().apply(block).build()
fun <T : Enum<*>> enumParameter(block: EnumParameterBuilder<T>.() -> Unit
): EnumParameter<T> = EnumParameterBuilder<T>().apply(block).build()
@@ -195,6 +195,15 @@ open class Template(@StringRes open val templateName: Int,
open val parameters: Collection<Parameter<*>>
get() = widgets.filterIsInstance<ParameterWidget<*>>().map { it.parameter }
companion object {
@JvmStatic
private val EMPTY_RECIPE: TemplateRecipe = {}
@JvmStatic
val EMPTY = Template(-1, -1, -1, emptyList(), EMPTY_RECIPE)
}
}
open class ProjectTemplate(val moduleTemplates: List<Template>,
+1
View File
@@ -33,6 +33,7 @@ dependencies {
implementation(projects.shared)
implementation(projects.common)
implementation(projects.preferences)
implementation(projects.subprojects.projects)
implementation(libs.androidx.annotation)
implementation(libs.androidx.ktx)
implementation(libs.google.auto.service.annotations)
@@ -0,0 +1,190 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.templates.impl
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.templates.ParameterConstraint
import com.itsaky.androidide.templates.ParameterConstraint.CLASS
import com.itsaky.androidide.templates.ParameterConstraint.CLASS_NAME
import com.itsaky.androidide.templates.ParameterConstraint.DIRECTORY
import com.itsaky.androidide.templates.ParameterConstraint.EXISTS
import com.itsaky.androidide.templates.ParameterConstraint.FILE
import com.itsaky.androidide.templates.ParameterConstraint.LAYOUT
import com.itsaky.androidide.templates.ParameterConstraint.MODULE_NAME
import com.itsaky.androidide.templates.ParameterConstraint.NONEMPTY
import com.itsaky.androidide.templates.ParameterConstraint.PACKAGE
import com.itsaky.androidide.templates.base.util.isValidModuleName
import com.itsaky.androidide.templates.impl.R.string
import com.itsaky.androidide.utils.AndroidUtils
import jdkx.lang.model.SourceVersion
import java.io.File
/**
* Verifies constraint for text field widget.
*
* @author Akash Yadav
*/
object ConstraintVerifier {
/**
* Checks whether the given input is valid against the given constraints.
*
* @see verify
*/
fun isValid(input: String, constraints: List<ParameterConstraint>): Boolean =
verify(input, constraints) == null
/**
* Verify the input against the given constraints.
*
* @param input The input to verify.
* @param constraints The constraints to verify against.
* @return The error message if the validation fails, `null` otherwise.
*/
fun verify(input: String, constraints: List<ParameterConstraint>): String? {
var err: String? = null
for (constraint in constraints) {
err = when (constraint) {
NONEMPTY -> checkNotEmpty(input)
PACKAGE -> validatePackageName(input)
CLASS -> validateClassName(input)
CLASS_NAME -> validateSimpleName(input)
MODULE_NAME -> validateModuleName(input)
LAYOUT -> validateLayoutName(input)
EXISTS -> checKFileExists(input)
FILE -> checkIsFile(input)
DIRECTORY -> checkIsDirectory(input)
else -> throw IllegalArgumentException(
"Unsupported parameter constraint '$constraint'")
}
if (err != null) {
return err
}
}
return null
}
private fun validatePackageName(input: String): String? {
AndroidUtils.validatePackageName(input)?.let { return it }
if (SourceVersion.isName(input)) {
return null
}
return BaseApplication.getBaseInstance()
.getString(R.string.msg_package_is_not_valid)
}
private fun validateLayoutName(input: String): String? {
if (input.isBlank()) {
return BaseApplication.getBaseInstance()
.getString(R.string.msg_value_empty)
}
// Allow only lowecase letters with digits and underscores
if (input.matches(Regex("[a-z][a-z0-9_]+"))) {
return null
}
return BaseApplication.getBaseInstance()
.getString(string.msg_invalid_layout_name)
}
private fun checkIsDirectory(input: String): String? {
checKFileExists(input)?.let { return it }
val file = File(input)
if (file.isDirectory) {
return null
}
return BaseApplication.getBaseInstance()
.getString(string.msg_path_must_be_dir)
}
private fun checkIsFile(input: String): String? {
checKFileExists(input)?.let { return it }
val file = File(input)
if (file.isFile) {
return null
}
return BaseApplication.getBaseInstance()
.getString(string.msg_path_must_be_file)
}
private fun checKFileExists(input: String): String? {
val file = File(input)
if (file.exists()) {
return null
}
return BaseApplication.getBaseInstance()
.getString(string.msg_file_not_exist)
}
private fun checkNotEmpty(input: String): String? {
if (input.isNotBlank()) {
return null
}
return BaseApplication.getBaseInstance().getString(string.msg_value_empty)
}
private fun validateModuleName(input: String): String? {
if (isValidModuleName(input)) {
return null
}
return BaseApplication.getBaseInstance()
.getString(string.msg_invalid_module_name)
}
private fun validateClassName(name: String): String? {
if (!name.contains('.')) {
return validateSimpleName(name)
}
val pck = name.substring(0, name.lastIndexOf('.'))
var err: String? = null
if (pck.contains('.')) {
err = validatePackageName(pck)
} else {
if (!SourceVersion.isIdentifier(pck) || SourceVersion.isKeyword(pck)) {
err = BaseApplication.getBaseInstance()
.getString(string.msg_package_is_not_valid)
}
}
if (err != null) {
return err
}
return validateSimpleName(name.substring(name.lastIndexOf('.') + 1))
}
private fun validateSimpleName(name: String): String? {
if (SourceVersion.isKeyword(name) || !SourceVersion.isIdentifier(name)) {
return BaseApplication.getBaseInstance()
.getString(string.msg_classname_with_keywords)
}
return null
}
}
@@ -0,0 +1,168 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.templates.impl
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.ArrayAdapter
import androidx.core.content.ContextCompat
import com.google.android.material.textfield.TextInputLayout
import com.google.auto.service.AutoService
import com.itsaky.androidide.templates.BooleanParameter
import com.itsaky.androidide.templates.CheckBoxWidget
import com.itsaky.androidide.templates.EnumParameter
import com.itsaky.androidide.templates.ITemplateWidgetViewProvider
import com.itsaky.androidide.templates.SpinnerWidget
import com.itsaky.androidide.templates.StringParameter
import com.itsaky.androidide.templates.TextFieldParameter
import com.itsaky.androidide.templates.TextFieldWidget
import com.itsaky.androidide.templates.Widget
import com.itsaky.androidide.templates.impl.databinding.LayoutCheckboxBinding
import com.itsaky.androidide.templates.impl.databinding.LayoutSpinnerBinding
import com.itsaky.androidide.templates.impl.databinding.LayoutTextfieldBinding
import com.itsaky.androidide.utils.ServiceLoader
import com.itsaky.androidide.utils.SingleTextWatcher
/**
* Default implementation of [ITemplateWidgetViewProvider].
*
* @author Akash Yadav
*/
@AutoService(ITemplateWidgetViewProvider::class)
class TemplateWidgetViewProviderImpl : ITemplateWidgetViewProvider {
companion object {
@JvmStatic
private var service: ITemplateWidgetViewProvider? = null
@JvmStatic
fun getInstance(reload: Boolean = false): ITemplateWidgetViewProvider {
if (reload) {
service = null
}
return ServiceLoader.load(ITemplateWidgetViewProvider::class.java)
.findFirstOrThrow()
.also { service = it }
}
}
override fun createView(context: Context, widget: Widget<*>): View {
return when (widget) {
is TextFieldWidget -> createTextField(context, widget)
is CheckBoxWidget -> createCheckBox(context, widget)
is SpinnerWidget -> createSpinner(context, widget)
else -> throw IllegalArgumentException("Unknown widget type : $widget")
}
}
private fun createCheckBox(context: Context, widget: CheckBoxWidget): View {
return LayoutCheckboxBinding.inflate(LayoutInflater.from(context)).apply {
val param = widget.parameter as BooleanParameter
root.setText(param.name)
root.isChecked = param.default
root.addOnCheckedStateChangedListener { checkbox, _ ->
param.value = checkbox.isChecked
}
}.root
}
private fun createTextField(context: Context, widget: TextFieldWidget): View {
return LayoutTextfieldBinding.inflate(LayoutInflater.from(context)).apply {
val param = widget.parameter as StringParameter
param.configureTextField(context, root) { value ->
param.value = value
val err =
ConstraintVerifier.verify(value, constraints = param.constraints)
root.isErrorEnabled = err != null
if (err != null) {
root.error = err
}
}
input.setText(param.default)
}.root
}
private fun createSpinner(context: Context, widget: SpinnerWidget<*>): View {
return LayoutSpinnerBinding.inflate(LayoutInflater.from(context)).apply {
val param = widget.parameter as EnumParameter<Enum<*>>
val items = mutableMapOf<String, Enum<*>>()
param.default.javaClass.enumConstants?.forEach {
println("Enum constant: $it")
val displayName = param.displayName?.invoke(it) ?: it.name
items[displayName] = it
}
check(
items.isNotEmpty()) { "Cannot retrive values for enum parameter $param with default value ${param.default}" }
val array = items.keys.toTypedArray()
println("EnumParam entries : ${array.joinToString(", ")}")
input.setAdapter(ArrayAdapter(context,
androidx.appcompat.R.layout.support_simple_spinner_dropdown_item,
array))
val defaultName =
param.displayName?.invoke(param.default) ?: param.default.name
root.isEnabled = items.size > 1
input.listSelection = array.indexOf(defaultName)
input.setText(defaultName, false)
param.configureTextField(context, root) {
param.value = items[it] ?: param.default
}
}.root
}
private fun TextFieldParameter<*>.configureTextField(context: Context,
root: TextInputLayout,
onTextChanged: (String) -> Unit = {}
) {
root.setHint(name)
startIcon?.let {
root.startIconDrawable = ContextCompat.getDrawable(context, it)
onStartIconClick?.let { onClick ->
root.setStartIconOnClickListener { onClick() }
}
}
endIcon?.let {
root.endIconDrawable = ContextCompat.getDrawable(context, it)
onEndIconClick?.let { onClick ->
root.setEndIconOnClickListener { onClick() }
}
}
root.editText!!.addTextChangedListener(object : SingleTextWatcher() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int,
count: Int
) {
onTextChanged(s?.toString() ?: "")
}
})
}
}
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<com.google.android.material.checkbox.MaterialCheckBox xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Use Kotlin Script (.kts)" />
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:hint="Choose value">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:hint="Project name">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.textfield.TextInputLayout>
@@ -18,22 +18,123 @@
package com.itsaky.androidide.templates
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.templates.ParameterConstraint.CLASS
import com.itsaky.androidide.templates.ParameterConstraint.CLASS_NAME
import com.itsaky.androidide.templates.ParameterConstraint.DIRECTORY
import com.itsaky.androidide.templates.ParameterConstraint.EXISTS
import com.itsaky.androidide.templates.ParameterConstraint.FILE
import com.itsaky.androidide.templates.ParameterConstraint.LAYOUT
import com.itsaky.androidide.templates.ParameterConstraint.MODULE_NAME
import com.itsaky.androidide.templates.ParameterConstraint.NONEMPTY
import com.itsaky.androidide.templates.ParameterConstraint.PACKAGE
import com.itsaky.androidide.templates.base.util.isValidModuleName
import com.itsaky.androidide.templates.base.util.moduleNameToDirName
import com.itsaky.androidide.templates.impl.ConstraintVerifier
import com.itsaky.androidide.utils.FileProvider
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.io.File
/**
* @author Akash Yadav
*/
@RunWith(RobolectricTestRunner::class)
@Config(application = BaseApplication::class)
class UtilTest {
@Test
fun `test module name conversion`() {
val tests = mapOf("2app" to "app", "app2" to "app2", "2app2" to "app2", "2 app2" to "app2",
"app name" to "app-name", "app name" to "app-name", "app-name" to "app-name",
"app--name" to "app-name")
val tests = mapOf("2app" to "app", "app2" to "app2", "2app2" to "app2",
"2 app2" to "app2", "app name" to "app-name", "app name" to "app-name",
"app-name" to "app-name", "app--name" to "app-name",
"my_module" to "my_module")
tests.forEach { (input, expected) ->
assertThat(moduleNameToDirName(input)).isEqualTo(expected)
}
}
@Test
fun `test module name validation`() {
val tests = mapOf("2app" to false, "app2" to true, "2app2" to false,
"2 app2" to false, "app name" to false, "app name" to false,
"app-name" to true, "app--name" to false)
tests.forEach { (name, result) ->
println("Check $name")
assertThat(isValidModuleName(":$name")).isEqualTo(result)
}
}
@Test
fun `test constraint verifier`() {
ConstraintVerifier.apply {
assertThat(isValid("", listOf(NONEMPTY))).isFalse()
assertThat(isValid("something", listOf(NONEMPTY))).isTrue()
assertThat(isValid("activity_main", listOf(LAYOUT))).isTrue()
assertThat(isValid("2activity_main", listOf(LAYOUT))).isFalse()
assertThat(isValid("2.invalid.package", listOf(PACKAGE))).isFalse()
assertThat(isValid("invalid", listOf(PACKAGE))).isFalse()
assertThat(isValid("2invalid.package", listOf(PACKAGE))).isFalse()
assertThat(isValid("invalid.package", listOf(PACKAGE))).isFalse()
assertThat(isValid("inval0d.PacKage", listOf(PACKAGE))).isFalse()
assertThat(isValid("com.itsaky.androidide", listOf(PACKAGE))).isTrue()
assertThat(isValid("Class", listOf(CLASS))).isTrue()
assertThat(isValid("pck.name.Class", listOf(CLASS))).isTrue()
assertThat(isValid("pck.name.Class_Name", listOf(CLASS))).isTrue()
assertThat(isValid("pck.name.Class____Name", listOf(CLASS))).isTrue()
assertThat(isValid("p443ackage.Class_Name", listOf(CLASS))).isTrue()
assertThat(isValid("package.2Class", listOf(CLASS))).isFalse()
assertThat(isValid("package.Class", listOf(CLASS))).isFalse()
assertThat(isValid("package.name.Class", listOf(CLASS))).isFalse()
assertThat(isValid("ClassName", listOf(CLASS_NAME))).isTrue()
assertThat(isValid("classname", listOf(CLASS_NAME))).isTrue()
assertThat(isValid("class_name", listOf(CLASS_NAME))).isTrue()
assertThat(isValid("class__name", listOf(CLASS_NAME))).isTrue()
assertThat(isValid("2class__name", listOf(CLASS_NAME))).isFalse()
assertThat(isValid("2class.name", listOf(CLASS_NAME))).isFalse()
assertThat(isValid(":app", listOf(MODULE_NAME))).isTrue()
assertThat(isValid(":app-name", listOf(MODULE_NAME))).isTrue()
assertThat(isValid(":app_name", listOf(MODULE_NAME))).isTrue()
assertThat(isValid(":my_module_num_2", listOf(MODULE_NAME))).isTrue()
assertThat(isValid(":2app", listOf(MODULE_NAME))).isFalse()
assertThat(isValid("2app", listOf(MODULE_NAME))).isFalse()
assertThat(isValid(":_app", listOf(MODULE_NAME))).isFalse()
assertThat(isValid("activity_main", listOf(LAYOUT))).isTrue()
assertThat(isValid("fragment__main", listOf(LAYOUT))).isTrue()
assertThat(isValid("layout_main", listOf(LAYOUT))).isTrue()
assertThat(isValid("Activity_Main", listOf(LAYOUT))).isFalse()
assertThat(isValid("ActivityMain", listOf(LAYOUT))).isFalse()
assertThat(isValid("2activity_main", listOf(LAYOUT))).isFalse()
assertThat(isValid("_activity_main", listOf(LAYOUT))).isFalse()
val build = FileProvider.currentDir().resolve("build").toFile()
val file = File(build, "constraint_test_file.txt").also { it.writeText("Test file") }
val nonExisting = File(build, "non_existing_constraint_test_file.txt")
assertThat(isValid(build.absolutePath, listOf(EXISTS))).isTrue()
assertThat(isValid(build.absolutePath, listOf(DIRECTORY))).isTrue()
assertThat(isValid(build.absolutePath, listOf(EXISTS, DIRECTORY))).isTrue()
assertThat(isValid(build.absolutePath, listOf(FILE))).isFalse()
assertThat(isValid(file.absolutePath, listOf(EXISTS))).isTrue()
assertThat(isValid(file.absolutePath, listOf(FILE))).isTrue()
assertThat(isValid(file.absolutePath, listOf(EXISTS, FILE))).isTrue()
assertThat(isValid(file.absolutePath, listOf(DIRECTORY))).isFalse()
assertThat(isValid(nonExisting.absolutePath, listOf(EXISTS))).isFalse()
assertThat(isValid(nonExisting.absolutePath, listOf(FILE))).isFalse()
assertThat(isValid(nonExisting.absolutePath, listOf(EXISTS, FILE))).isFalse()
}
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.nav.frag)
implementation(libs.androidx.nav.fragment)
implementation(libs.androidx.nav.ui)
implementation(libs.androidx.nav.dynamic.features)
implementation(libs.common.editor)