mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat: use Flashbar for project sync notification
This commit is contained in:
@@ -21,9 +21,11 @@ import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.itsaky.androidide.actions.ActionData
|
||||
import com.itsaky.androidide.actions.EditorRelatedAction
|
||||
import com.itsaky.androidide.models.SaveResult
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.androidide.utils.flashError
|
||||
import com.itsaky.androidide.utils.flashSuccess
|
||||
|
||||
/** @author Akash Yadav */
|
||||
@@ -52,34 +54,40 @@ class SaveFileAction(context: Context) : EditorRelatedAction() {
|
||||
enabled = context.areFilesModified()
|
||||
}
|
||||
|
||||
override fun execAction(data: ActionData): Boolean {
|
||||
val context = data.getActivity() ?: return false
|
||||
override fun execAction(data: ActionData): ResultWrapper {
|
||||
val context = data.getActivity() ?: return ResultWrapper()
|
||||
|
||||
return try {
|
||||
// Cannot use context.saveAll() because this.execAction is called on non-UI thread
|
||||
// and saveAll call will result in UI actions
|
||||
val result = context.saveAllResult()
|
||||
|
||||
if (result.xmlSaved) {
|
||||
ProjectManager.generateSources()
|
||||
}
|
||||
|
||||
if (result.gradleSaved) {
|
||||
context.notifySyncNeeded()
|
||||
}
|
||||
true
|
||||
ResultWrapper(context.saveAllResult())
|
||||
} catch (error: Throwable) {
|
||||
log.error("Failed to save file", error)
|
||||
false
|
||||
ResultWrapper()
|
||||
}
|
||||
}
|
||||
|
||||
override fun postExec(data: ActionData, result: Any) {
|
||||
if (result is Boolean && result) {
|
||||
if (result is ResultWrapper && result.result != null) {
|
||||
val context = data.requireActivity()
|
||||
|
||||
// show save notification before calling 'notifySyncNeeded' so that the file save notification
|
||||
// does not overlap the sync notification
|
||||
flashSuccess(R.string.all_saved)
|
||||
|
||||
val saveResult = result.result
|
||||
if (saveResult.xmlSaved) {
|
||||
ProjectManager.generateSources()
|
||||
}
|
||||
|
||||
if (saveResult.gradleSaved) {
|
||||
context.notifySyncNeeded()
|
||||
}
|
||||
} else {
|
||||
log.error("Failed to save file")
|
||||
TODO("Create message in strings.xml")
|
||||
flashError(R.string.save_failed)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ResultWrapper(val result: SaveResult? = null)
|
||||
}
|
||||
|
||||
@@ -80,16 +80,19 @@ import com.itsaky.androidide.projects.ProjectManager.getProjectDirPath
|
||||
import com.itsaky.androidide.projects.ProjectManager.projectPath
|
||||
import com.itsaky.androidide.projects.builder.BuildService
|
||||
import com.itsaky.androidide.services.LogReceiver
|
||||
import com.itsaky.androidide.ui.MaterialBanner
|
||||
import com.itsaky.androidide.ui.editor.CodeEditorView
|
||||
import com.itsaky.androidide.uidesigner.UIDesignerActivity
|
||||
import com.itsaky.androidide.utils.ActionMenuUtils.createMenu
|
||||
import com.itsaky.androidide.utils.ApkInstallationSessionCallback
|
||||
import com.itsaky.androidide.utils.DURATION_INDEFINITE
|
||||
import com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.androidide.utils.InstallationResultHandler.onResult
|
||||
import com.itsaky.androidide.utils.flashError
|
||||
import com.itsaky.androidide.utils.flashbarBuilder
|
||||
import com.itsaky.androidide.utils.resolveAttr
|
||||
import com.itsaky.androidide.utils.showOnUiThread
|
||||
import com.itsaky.androidide.utils.withIcon
|
||||
import com.itsaky.androidide.viewmodel.EditorViewModel
|
||||
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
|
||||
import com.itsaky.androidide.xml.versions.ApiVersionsRegistry
|
||||
@@ -191,13 +194,11 @@ abstract class BaseEditorActivity :
|
||||
val packageName = onResult(this, intent)
|
||||
if (packageName != null) {
|
||||
Snackbar.make(binding.realContainer, string.msg_action_open_application, Snackbar.LENGTH_LONG)
|
||||
.setAction(string.yes) {
|
||||
tryLaunchApp(packageName)
|
||||
}
|
||||
.setAction(string.yes) { tryLaunchApp(packageName) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -385,13 +386,23 @@ abstract class BaseEditorActivity :
|
||||
|
||||
fun notifySyncNeeded(onConfirm: () -> Unit) {
|
||||
val buildService = Lookup.DEFAULT.lookup(BuildService.KEY_BUILD_SERVICE)
|
||||
if (buildService != null && !buildService.isBuildInProgress) {
|
||||
getSyncBanner()?.apply {
|
||||
setNegative(android.R.string.cancel, null)
|
||||
setPositive(android.R.string.ok) { onConfirm() }
|
||||
show()
|
||||
if (buildService == null || buildService.isBuildInProgress) return
|
||||
|
||||
flashbarBuilder(
|
||||
duration = DURATION_INDEFINITE,
|
||||
backgroundColor = resolveAttr(attr.colorSecondaryContainer),
|
||||
messageColor = resolveAttr(attr.colorOnSecondaryContainer)
|
||||
)
|
||||
.withIcon(drawable.ic_sync, colorFilter = resolveAttr(attr.colorOnSecondaryContainer))
|
||||
.message(string.msg_sync_needed)
|
||||
.positiveActionText(string.btn_sync)
|
||||
.positiveActionTapListener {
|
||||
onConfirm()
|
||||
it.dismiss()
|
||||
}
|
||||
}
|
||||
.negativeActionText(string.btn_ignore_changes)
|
||||
.negativeActionTapListener { it.dismiss() }
|
||||
.showOnUiThread()
|
||||
}
|
||||
|
||||
open fun getFileTreeFragment(): FileTreeFragment? {
|
||||
@@ -406,7 +417,7 @@ abstract class BaseEditorActivity :
|
||||
viewModel.statusText = text
|
||||
viewModel.statusGravity = gravity
|
||||
}
|
||||
|
||||
|
||||
private fun tryLaunchApp(packageName: String) {
|
||||
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
launchIntent?.let {
|
||||
@@ -629,17 +640,6 @@ abstract class BaseEditorActivity :
|
||||
builder.create().show()
|
||||
}
|
||||
|
||||
private fun getSyncBanner(): MaterialBanner? {
|
||||
return binding.run {
|
||||
return@run syncBanner
|
||||
.setContentTextColor(resolveAttr(attr.colorOnPrimaryContainer))
|
||||
.setBannerBackgroundColor(resolveAttr(attr.colorPrimaryContainer))
|
||||
.setButtonTextColor(resolveAttr(attr.colorOnPrimaryContainer))
|
||||
.setIcon(drawable.ic_sync)
|
||||
.setContentText(string.msg_sync_needed)
|
||||
}
|
||||
}
|
||||
|
||||
open fun installationSessionCallback(): SessionCallback {
|
||||
return ApkInstallationSessionCallback(this).also { installationCallback = it }
|
||||
}
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
/************************************************************************************
|
||||
* 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.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.Transformation;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.itsaky.androidide.databinding.LayoutBannerBinding;
|
||||
|
||||
public class MaterialBanner extends CoordinatorLayout {
|
||||
|
||||
private LayoutBannerBinding binding;
|
||||
private OnPositiveClickListener positiveListener;
|
||||
private OnNegativeClickListener negativeListener;
|
||||
|
||||
private String contentText;
|
||||
private String leftButtonText;
|
||||
private String rightButtonText;
|
||||
private Drawable iconDrawable;
|
||||
private int backgroundColor;
|
||||
private int contentTextColor;
|
||||
private int buttonTextColor;
|
||||
|
||||
public MaterialBanner(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public MaterialBanner(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public MaterialBanner(Context context, AttributeSet attrs, int style) {
|
||||
super(context, attrs, style);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
binding = LayoutBannerBinding.inflate(LayoutInflater.from(getContext()), this, true);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
this.binding.contentTextView.setText(contentText);
|
||||
this.binding.contentIconView.setImageDrawable(iconDrawable);
|
||||
this.binding.rightButton.setText(rightButtonText);
|
||||
this.binding.leftButton.setText(leftButtonText);
|
||||
this.binding.getRoot().setBackgroundColor(backgroundColor);
|
||||
this.binding.contentTextView.setTextColor(contentTextColor);
|
||||
this.binding.leftButton.setTextColor(buttonTextColor);
|
||||
this.binding.rightButton.setTextColor(buttonTextColor);
|
||||
this.binding.rightButton.setOnClickListener(
|
||||
v -> {
|
||||
if (positiveListener == null) dismiss();
|
||||
else {
|
||||
positiveListener.onClick(MaterialBanner.this);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
this.binding.leftButton.setOnClickListener(
|
||||
v -> {
|
||||
if (negativeListener == null) dismiss();
|
||||
else {
|
||||
negativeListener.onClick(MaterialBanner.this);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
final ConstraintLayout root = this.binding.getRoot();
|
||||
root.measure(-1, -2);
|
||||
final int height = root.getMeasuredHeight();
|
||||
final LayoutParams p = (LayoutParams) root.getLayoutParams();
|
||||
p.height = 0;
|
||||
root.setLayoutParams(p);
|
||||
root.setVisibility(VISIBLE);
|
||||
setVisibility(VISIBLE);
|
||||
|
||||
Animation anim =
|
||||
new Animation() {
|
||||
@Override
|
||||
public boolean willChangeBounds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyTransformation(float time, Transformation t) {
|
||||
p.height = time == 1f ? -2 : (int) (height * time);
|
||||
root.setLayoutParams(p);
|
||||
root.requestLayout();
|
||||
}
|
||||
};
|
||||
|
||||
anim.setDuration(200);
|
||||
root.startAnimation(anim);
|
||||
}
|
||||
|
||||
public void dismiss() {
|
||||
final ConstraintLayout root = this.binding.getRoot();
|
||||
final LayoutParams p = (LayoutParams) root.getLayoutParams();
|
||||
final int initialHeight = getMeasuredHeight();
|
||||
Animation animation =
|
||||
new Animation() {
|
||||
@Override
|
||||
public boolean willChangeBounds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
if (interpolatedTime == 1f) {
|
||||
root.setVisibility(GONE);
|
||||
setVisibility(GONE);
|
||||
} else {
|
||||
p.height = (int) (initialHeight - (initialHeight * interpolatedTime));
|
||||
root.requestLayout();
|
||||
}
|
||||
}
|
||||
};
|
||||
animation.setDuration((long) (initialHeight / getResources().getDisplayMetrics().density));
|
||||
root.startAnimation(animation);
|
||||
}
|
||||
|
||||
public MaterialBanner setIcon(@DrawableRes int icon) {
|
||||
return setIcon(ContextCompat.getDrawable(getContext(), icon));
|
||||
}
|
||||
|
||||
public MaterialBanner setIcon(Drawable icon) {
|
||||
this.iconDrawable = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MaterialBanner setPositive(@StringRes int text, OnPositiveClickListener listener) {
|
||||
return setPositive(getContext().getString(text), listener);
|
||||
}
|
||||
|
||||
public MaterialBanner setPositive(String text, OnPositiveClickListener listener) {
|
||||
this.rightButtonText = text;
|
||||
this.positiveListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MaterialBanner setNegative(@StringRes int text, OnNegativeClickListener listener) {
|
||||
return setNegative(getContext().getString(text), listener);
|
||||
}
|
||||
|
||||
public MaterialBanner setNegative(String text, OnNegativeClickListener listener) {
|
||||
this.leftButtonText = text;
|
||||
this.negativeListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MaterialBanner setContentText(@StringRes int text) {
|
||||
return setContentText(getContext().getString(text));
|
||||
}
|
||||
|
||||
public MaterialBanner setContentText(String contentText) {
|
||||
this.contentText = contentText;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MaterialBanner setBannerBackgroundColor(int backgroundColor) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MaterialBanner setContentTextColor(int contentTextColor) {
|
||||
this.contentTextColor = contentTextColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MaterialBanner setButtonTextColor(int buttonTextColor) {
|
||||
this.buttonTextColor = buttonTextColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public static interface OnPositiveClickListener {
|
||||
public void onClick(MaterialBanner banner);
|
||||
}
|
||||
|
||||
public static interface OnNegativeClickListener {
|
||||
public void onClick(MaterialBanner banner);
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true" />
|
||||
|
||||
<com.itsaky.androidide.ui.MaterialBanner
|
||||
android:id="@+id/syncBanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
style="@style/AppTheme.TabLayout"
|
||||
|
||||
@@ -25,10 +25,10 @@ dependencies {
|
||||
implementation(projects.eventbusEvents)
|
||||
implementation(projects.lexers)
|
||||
implementation(projects.resources)
|
||||
implementation(projects.subprojects.flashbar)
|
||||
|
||||
api(projects.shared)
|
||||
api(projects.logger)
|
||||
api(projects.subprojects.flashbar)
|
||||
|
||||
testImplementation(libs.tests.junit)
|
||||
testImplementation(libs.tests.google.truth)
|
||||
|
||||
@@ -36,19 +36,25 @@ import com.itsaky.androidide.utils.FlashType.ERROR
|
||||
import com.itsaky.androidide.utils.FlashType.INFO
|
||||
import com.itsaky.androidide.utils.FlashType.SUCCESS
|
||||
|
||||
const val LENGTH_SHORT = 2000L
|
||||
const val LENGTH_LONG = 3500L
|
||||
const val DURATION_SHORT = 2000L
|
||||
const val DURATION_LONG = 3500L
|
||||
const val DURATION_INDEFINITE = Flashbar.DURATION_INDEFINITE
|
||||
|
||||
private val COLOR_SUCCESS = Color.parseColor("#4CAF50")
|
||||
private val COLOR_ERROR = Color.parseColor("#f44336")
|
||||
private const val COLOR_INFO = Color.DKGRAY
|
||||
val COLOR_SUCCESS = Color.parseColor("#4CAF50")
|
||||
val COLOR_ERROR = Color.parseColor("#f44336")
|
||||
const val COLOR_INFO = Color.DKGRAY
|
||||
|
||||
fun Activity.flashbarBuilder(duration: Long = LENGTH_SHORT): Flashbar.Builder {
|
||||
fun Activity.flashbarBuilder(
|
||||
gravity: Flashbar.Gravity = TOP,
|
||||
duration: Long = DURATION_SHORT,
|
||||
backgroundColor: Int = resolveAttr(R.attr.colorPrimaryContainer),
|
||||
messageColor: Int = resolveAttr(R.attr.colorOnPrimaryContainer)
|
||||
): Flashbar.Builder {
|
||||
return Flashbar.Builder(this)
|
||||
.gravity(TOP)
|
||||
.gravity(gravity)
|
||||
.duration(duration)
|
||||
.backgroundColor(resolveAttr(R.attr.colorPrimaryContainer))
|
||||
.messageColor(resolveAttr(R.attr.colorOnPrimaryContainer))
|
||||
.backgroundColor(backgroundColor)
|
||||
.messageColor(messageColor)
|
||||
}
|
||||
|
||||
fun Activity.flashMessage(msg: String?, type: FlashType) {
|
||||
@@ -107,19 +113,19 @@ fun Flashbar.showOnUiThread() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flashbar.Builder.successIcon(): Flashbar.Builder {
|
||||
fun Flashbar.Builder.successIcon(): Flashbar.Builder {
|
||||
return withIcon(R.drawable.ic_ok, colorFilter = COLOR_SUCCESS)
|
||||
}
|
||||
|
||||
private fun Flashbar.Builder.errorIcon(): Flashbar.Builder {
|
||||
fun Flashbar.Builder.errorIcon(): Flashbar.Builder {
|
||||
return withIcon(R.drawable.ic_error, colorFilter = COLOR_ERROR)
|
||||
}
|
||||
|
||||
private fun Flashbar.Builder.infoIcon(): Flashbar.Builder {
|
||||
fun Flashbar.Builder.infoIcon(): Flashbar.Builder {
|
||||
return withIcon(R.drawable.ic_info, colorFilter = COLOR_INFO)
|
||||
}
|
||||
|
||||
private fun Flashbar.Builder.withIcon(
|
||||
fun Flashbar.Builder.withIcon(
|
||||
@DrawableRes icon: Int,
|
||||
@FloatRange(from = 0.0, to = 1.0) scale: Float = 1.0f,
|
||||
@ColorInt colorFilter: Int = -1,
|
||||
@@ -131,4 +137,4 @@ private fun Flashbar.Builder.withIcon(
|
||||
iconColorFilter(colorFilter, colorFilterMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
<string name="action_closeOthers">Close others</string>
|
||||
<string name="action_closeThis">Close this</string>
|
||||
<string name="all_saved">All files saved!</string>
|
||||
<string name="save_failed">Failed to save files</string>
|
||||
<string name="msg_newfile_dest">Destination: %s/</string>
|
||||
<string name="restype_drawable">Drawable</string>
|
||||
<string name="restype_layout">Layout</string>
|
||||
@@ -539,4 +540,7 @@
|
||||
<string name="idepref_deleteEmptyLines_summary">Whether the editor should delete empty lines on a single backspace key event.</string>
|
||||
<string name="idepref_deleteTabs_title">Delete tab on backspace</string>
|
||||
<string name="idepref_deleteTabs_summary">When enabled, the editor will delete spaces equal to the tab width when the backspace key is pressed.</string>
|
||||
|
||||
<string name="btn_sync">Sync</string>
|
||||
<string name="btn_ignore_changes">Ignore these changes</string>
|
||||
</resources>
|
||||
|
||||
@@ -652,7 +652,7 @@ class Flashbar private constructor(private var builder: Builder) {
|
||||
RIGHT
|
||||
}
|
||||
|
||||
interface OnActionTapListener {
|
||||
fun interface OnActionTapListener {
|
||||
fun onActionTapped(bar: Flashbar)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user