diff --git a/Habitica/res/drawable/layout_rounded_bg_header_bar.xml b/Habitica/res/drawable/layout_rounded_bg_header_bar.xml deleted file mode 100644 index 3ab4bfac4..000000000 --- a/Habitica/res/drawable/layout_rounded_bg_header_bar.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Habitica/res/drawable/task_form_control_bg.xml b/Habitica/res/drawable/task_form_control_bg.xml index 66ba48871..612626f02 100644 --- a/Habitica/res/drawable/task_form_control_bg.xml +++ b/Habitica/res/drawable/task_form_control_bg.xml @@ -2,7 +2,7 @@ - + diff --git a/Habitica/res/layout/equipment_overview_item.xml b/Habitica/res/layout/equipment_overview_item.xml deleted file mode 100644 index ab8b9eff6..000000000 --- a/Habitica/res/layout/equipment_overview_item.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Habitica/res/layout/equipment_overview_view.xml b/Habitica/res/layout/equipment_overview_view.xml deleted file mode 100644 index eebc13b54..000000000 --- a/Habitica/res/layout/equipment_overview_view.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Habitica/res/layout/fragment_avatar_overview.xml b/Habitica/res/layout/fragment_avatar_overview.xml index 0a6b3a683..ec9322b44 100644 --- a/Habitica/res/layout/fragment_avatar_overview.xml +++ b/Habitica/res/layout/fragment_avatar_overview.xml @@ -49,12 +49,12 @@ android:layout_marginEnd="8dp" android:entries="@array/avatar_sizes"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Habitica/res/layout/fragment_skills.xml b/Habitica/res/layout/fragment_skills.xml deleted file mode 100644 index e10ba38a8..000000000 --- a/Habitica/res/layout/fragment_skills.xml +++ /dev/null @@ -1,9 +0,0 @@ - - diff --git a/Habitica/res/layout/task_form_habit_scoring.xml b/Habitica/res/layout/task_form_habit_scoring.xml deleted file mode 100644 index 8d1827b9f..000000000 --- a/Habitica/res/layout/task_form_habit_scoring.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/Habitica/res/layout/task_form_task_difficulty.xml b/Habitica/res/layout/task_form_task_difficulty.xml deleted file mode 100644 index f18c3390f..000000000 --- a/Habitica/res/layout/task_form_task_difficulty.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml index 95345f3f2..e1decdb78 100644 --- a/Habitica/res/values/strings.xml +++ b/Habitica/res/values/strings.xml @@ -1269,6 +1269,7 @@ Assign Edit assignees Assign to... + Promote to Manager You You, %d other diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt index c2f65d854..2ded4ccb3 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt @@ -61,6 +61,12 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife override fun onCreate() { super.onCreate() + if (!BuildConfig.DEBUG) { + try { + AmplitudeManager.initialize(this) + } catch (ignored: Resources.NotFoundException) { + } + } registerActivityLifecycleCallbacks(this) setupRealm() setupDagger() @@ -73,12 +79,7 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) - if (!BuildConfig.DEBUG) { - try { - AmplitudeManager.initialize(this, sharedPrefs) - } catch (ignored: Resources.NotFoundException) { - } - } + setupCoil() ExceptionHandler.init(analyticsManager) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AmplitudeManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AmplitudeManager.kt index c8e33ed26..0b65ab681 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AmplitudeManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AmplitudeManager.kt @@ -51,14 +51,16 @@ object AmplitudeManager { sendEvent("navigated", EVENT_CATEGORY_NAVIGATION, EVENT_HITTYPE_PAGEVIEW, additionalData) } - fun initialize(context: Context, sharedPrefs: SharedPreferences) { + fun initialize(context: Context) { amplitude = Amplitude( Configuration( context.getString(R.string.amplitude_app_id), context ) ) + } + fun identify(sharedPrefs: SharedPreferences) { val identify = Identify() .setOnce("androidStore", BuildConfig.STORE) sharedPrefs.getString("launch_screen", "")?.let { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/Group.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/Group.kt index 7668413e6..eb5dcceca 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/Group.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/Group.kt @@ -55,12 +55,19 @@ open class Group : RealmObject(), BaseMainObject { } fun hasTaskEditPrivileges(userID: String): Boolean { - if (leaderID == userID) { + if (isLeader(userID)) { return true } - return managers?.contains(userID) == true + return isManager(userID) } + fun canManageManagers(userID: String): Boolean { + return isLeader(userID) + } + + fun isLeader(userID: String): Boolean = leaderID == userID + fun isManager(userID: String): Boolean = managers?.contains(userID) == true + companion object { const val TAVERN_ID = "00000000-0000-4000-A000-000000000000" } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt index 1513621ad..7ad85dcec 100755 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt @@ -16,6 +16,7 @@ import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.core.os.bundleOf import androidx.core.view.children import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.lifecycleScope @@ -53,8 +54,10 @@ import com.habitrpg.android.habitica.ui.theme.HabiticaTheme import com.habitrpg.android.habitica.ui.viewmodels.MainActivityViewModel import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel import com.habitrpg.android.habitica.ui.views.AppHeaderView +import com.habitrpg.android.habitica.ui.views.GroupPlanMemberList import com.habitrpg.android.habitica.ui.views.SnackbarActivity import com.habitrpg.android.habitica.ui.views.dialogs.QuestCompletedDialog +import com.habitrpg.android.habitica.ui.views.showAsBottomSheet import com.habitrpg.android.habitica.ui.views.yesterdailies.YesterdailyDialog import com.habitrpg.android.habitica.widget.AvatarStatsWidgetProvider import com.habitrpg.android.habitica.widget.DailiesWidgetProvider @@ -83,18 +86,25 @@ open class MainActivity : BaseActivity(), SnackbarActivity { @Inject internal lateinit var apiClient: ApiClient + @Inject internal lateinit var soundManager: SoundManager + @Inject internal lateinit var checkClassSelectionUseCase: CheckClassSelectionUseCase + @Inject internal lateinit var displayItemDropUseCase: DisplayItemDropUseCase + @Inject internal lateinit var notifyUserUseCase: NotifyUserUseCase + @Inject internal lateinit var taskRepository: TaskRepository + @Inject internal lateinit var inventoryRepository: InventoryRepository + @Inject internal lateinit var appConfigManager: AppConfigManager @@ -171,7 +181,8 @@ open class MainActivity : BaseActivity(), SnackbarActivity { } val drawerLayout = findViewById(R.id.drawer_layout) - drawerFragment = supportFragmentManager.findFragmentById(R.id.navigation_drawer) as? NavigationDrawerFragment + drawerFragment = + supportFragmentManager.findFragmentById(R.id.navigation_drawer) as? NavigationDrawerFragment drawerFragment?.setUp(R.id.navigation_drawer, drawerLayout, notificationsViewModel) drawerToggle = object : ActionBarDrawerToggle( @@ -191,7 +202,10 @@ open class MainActivity : BaseActivity(), SnackbarActivity { window.updateStatusBarColor(getThemeColor(R.attr.colorPrimaryDark), false) isOpeningDrawer = true } else if (slideOffset > 0.5f && isOpeningDrawer == null) { - window.updateStatusBarColor(getThemeColor(R.attr.headerBackgroundColor), true) + window.updateStatusBarColor( + getThemeColor(R.attr.headerBackgroundColor), + true + ) isOpeningDrawer = false } } @@ -221,10 +235,23 @@ open class MainActivity : BaseActivity(), SnackbarActivity { supportActionBar?.setHomeButtonEnabled(true) setupNotifications() setupBottomnavigationLayoutListener() - + binding.content.headerView.setContent { HabiticaTheme { - AppHeaderView(viewModel.userViewModel) + AppHeaderView(viewModel.userViewModel) { + showAsBottomSheet { onClose -> + GroupPlanMemberList(it, { + onClose() + FullProfileActivity.open(it) + }, { member -> + onClose() + MainNavigationController.navigate( + R.id.inboxMessageListFragment, + bundleOf(Pair("username", member.username), Pair("userID", member.id)) + ) + }) + } + } } } @@ -268,7 +295,12 @@ open class MainActivity : BaseActivity(), SnackbarActivity { private fun setupBottomnavigationLayoutListener() { binding.content.bottomNavigation.viewTreeObserver.addOnGlobalLayoutListener { if (binding.content.bottomNavigation.visibility == View.VISIBLE) { - snackbarContainer.setPadding(0, 0, 0, binding.content.bottomNavigation.barHeight + 12.dpToPx(this)) + snackbarContainer.setPadding( + 0, + 0, + 0, + binding.content.bottomNavigation.barHeight + 12.dpToPx(this) + ) } else { snackbarContainer.setPadding(0, 0, 0, 0) } @@ -310,10 +342,16 @@ open class MainActivity : BaseActivity(), SnackbarActivity { viewModel.onResume() - val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment val navigationController = navHostFragment.navController MainNavigationController.setup(navigationController) - navigationController.addOnDestinationChangedListener { _, destination, arguments -> updateToolbarTitle(destination, arguments) } + navigationController.addOnDestinationChangedListener { _, destination, arguments -> + updateToolbarTitle( + destination, + arguments + ) + } viewModel.requestNotificationPermission.observe(this) { requestNotificationPermission -> if (requestNotificationPermission && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)) { @@ -337,7 +375,11 @@ open class MainActivity : BaseActivity(), SnackbarActivity { } resumeFromActivity = false - if ((intent.hasExtra("notificationIdentifier") || intent.hasExtra("openURL")) && lastNotificationOpen != intent.getLongExtra("notificationTimeStamp", 0)) { + if ((intent.hasExtra("notificationIdentifier") || intent.hasExtra("openURL")) && lastNotificationOpen != intent.getLongExtra( + "notificationTimeStamp", + 0 + ) + ) { lastNotificationOpen = intent.getLongExtra("notificationTimeStamp", 0) val identifier = intent.getStringExtra("notificationIdentifier") ?: "" if (intent.hasExtra("sendAnalytics")) { @@ -387,7 +429,8 @@ open class MainActivity : BaseActivity(), SnackbarActivity { private fun updateWidget(widgetClass: Class<*>) { val intent = Intent(this, widgetClass) intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE - val ids = AppWidgetManager.getInstance(application).getAppWidgetIds(ComponentName(application, widgetClass)) + val ids = AppWidgetManager.getInstance(application) + .getAppWidgetIds(ComponentName(application, widgetClass)) intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) sendBroadcast(intent) } @@ -412,7 +455,9 @@ open class MainActivity : BaseActivity(), SnackbarActivity { val quest = user.party?.quest if (quest?.completed?.isNotBlank() == true) { lifecycleScope.launch(ExceptionHandler.coroutine()) { - val questContent = inventoryRepository.getQuestContent(user.party?.quest?.completed ?: "").firstOrNull() + val questContent = + inventoryRepository.getQuestContent(user.party?.quest?.completed ?: "") + .firstOrNull() if (questContent != null) { QuestCompletedDialog.showWithQuest(this@MainActivity, questContent) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt index ea95874cf..9b54c937a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt @@ -22,46 +22,11 @@ import android.view.WindowManager import android.widget.CheckBox import androidx.activity.viewModels import androidx.appcompat.widget.AppCompatCheckBox -import androidx.compose.animation.animateColor -import androidx.compose.animation.core.FastOutLinearInEasing -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.fadeIn -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.toMutableStateList -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.core.os.bundleOf import androidx.core.view.children import androidx.core.view.forEachIndexed @@ -79,7 +44,6 @@ import com.habitrpg.android.habitica.extensions.addCancelButton import com.habitrpg.android.habitica.helpers.ExceptionHandler import com.habitrpg.android.habitica.helpers.TaskAlarmManager import com.habitrpg.android.habitica.helpers.launchCatching -import com.habitrpg.android.habitica.models.Assignable import com.habitrpg.android.habitica.models.Tag import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.social.Challenge @@ -89,10 +53,10 @@ import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard import com.habitrpg.android.habitica.ui.theme.HabiticaTheme import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel import com.habitrpg.android.habitica.ui.viewmodels.TaskFormViewModel -import com.habitrpg.android.habitica.ui.views.CompletedAt -import com.habitrpg.android.habitica.ui.views.UserRow import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog import com.habitrpg.android.habitica.ui.views.showAsBottomSheet +import com.habitrpg.android.habitica.ui.views.tasks.AssignSheet +import com.habitrpg.android.habitica.ui.views.tasks.AssignedView import com.habitrpg.android.habitica.ui.views.tasks.form.HabitScoringSelector import com.habitrpg.android.habitica.ui.views.tasks.form.LabeledValue import com.habitrpg.android.habitica.ui.views.tasks.form.TaskDifficultySelector @@ -863,7 +827,7 @@ class TaskFormActivity : BaseActivity() { } private fun showAssignDialog() { - showAsBottomSheet { + showAsBottomSheet { onClose -> AssignSheet( groupMembers, assignedIDs, @@ -873,7 +837,8 @@ class TaskFormActivity : BaseActivity() { } else { assignedIDs.add(it) } - } + }, + onClose ) } } @@ -901,152 +866,3 @@ private fun String.toIntCatchOverflow(): Int? { 0 } } - -@Composable -fun AssignedView( - assigned: List, - completedAt: Map, - backgroundColor: Color, - color: Color, - onEditClick: () -> Unit, - modifier: Modifier = Modifier, - showEditButton: Boolean = false -) { - Column(modifier.fillMaxWidth()) { - val rowModifier = Modifier - .padding(vertical = 4.dp) - .background( - backgroundColor, - MaterialTheme.shapes.medium - ) - .padding(15.dp, 12.dp) - .heightIn(min = 24.dp) - .fillMaxWidth() - for (assignable in assigned) { - UserRow( - username = assignable.identifiableName, modifier = rowModifier, - color = color, - extraContent = { - completedAt[assignable.id]?.let { CompletedAt(completedAt = it) } - } - ) - } - if (showEditButton) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .clickable { - onEditClick() - } - .padding(vertical = 4.dp) - .background( - backgroundColor, - MaterialTheme.shapes.medium - ) - .padding(15.dp, 12.dp) - .heightIn(min = 24.dp) - .fillMaxWidth()) { - Image( - painterResource(R.drawable.edit), - null, - colorFilter = ColorFilter.tint(MaterialTheme.colors.primary) - ) - Text( - stringResource(R.string.edit_assignees), color = color, - modifier = Modifier.padding(start = 4.dp) - ) - } - } - } -} - -@Composable -fun AssignSheet( - members: List, - assignedMembers: List, - onAssignClick: (String) -> Unit, - modifier: Modifier = Modifier -) { - Column(modifier) { - Box { - Text( - stringResource(R.string.assign_to), - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = colorResource(R.color.gray_200), - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - ) - TextButton( - onClick = { - - }, - colors = ButtonDefaults.textButtonColors(), - modifier = Modifier.align(Alignment.CenterEnd) - ) { - Text(stringResource(R.string.done)) - } - } - for (member in members) { - val isAssigned = assignedMembers.contains(member.id) - val transition = updateTransition(isAssigned, label = "isAssigned") - val rotation = transition.animateFloat( - label = "isAssigned", - transitionSpec = { spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow) }) { - if (it) 0f else 45f - } - val backgroundColor = transition.animateColor( - label = "isAssigned", - transitionSpec = { tween(400, easing = FastOutLinearInEasing) }) { - if (it) MaterialTheme.colors.primary else colorResource(id = R.color.transparent) - } - val color = transition.animateColor( - label = "isAssigned", - transitionSpec = { tween(400, easing = FastOutLinearInEasing) }) { - fadeIn(tween(10000)) - colorResource(if (it) R.color.white else R.color.text_dimmed) - } - val borderColor = transition.animateColor( - label = "isAssigned", - transitionSpec = { tween(400, easing = FastOutLinearInEasing) }) { - fadeIn(tween(10000)) - if (it) MaterialTheme.colors.primary else colorResource(id = R.color.text_dimmed) - } - UserRow( - username = member.displayName, - color = colorResource(R.color.text_primary), - extraContent = { - Text( - member.formattedUsername ?: "", - color = colorResource(R.color.text_ternary) - ) - }, endContent = { - Image( - painterResource(R.drawable.ic_close_white_24dp), - null, - colorFilter = ColorFilter.tint(color.value), - modifier = Modifier - .rotate(rotation.value) - .size(24.dp) - .background( - backgroundColor.value, - CircleShape - ) - .border( - 2.dp, - borderColor.value, - CircleShape - ) - .padding(3.dp) - ) - }, modifier = Modifier - .clickable { - member.id?.let { onAssignClick(it) } - } - .padding(30.dp, 12.dp) - .heightIn(min = 24.dp) - .fillMaxWidth() - ) - } - } -} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/skills/SkillsFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/skills/SkillsFragment.kt index 1166cb6c3..6b7652bb6 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/skills/SkillsFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/skills/SkillsFragment.kt @@ -12,7 +12,7 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.components.UserComponent -import com.habitrpg.android.habitica.databinding.FragmentSkillsBinding +import com.habitrpg.android.habitica.databinding.FragmentRecyclerviewBinding import com.habitrpg.android.habitica.helpers.ExceptionHandler import com.habitrpg.android.habitica.helpers.launchCatching import com.habitrpg.android.habitica.models.Skill @@ -32,17 +32,17 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import javax.inject.Inject -class SkillsFragment : BaseMainFragment() { +class SkillsFragment : BaseMainFragment() { internal var adapter: SkillsRecyclerViewAdapter? = null private var selectedSkill: Skill? = null - override var binding: FragmentSkillsBinding? = null + override var binding: FragmentRecyclerviewBinding? = null @Inject lateinit var userViewModel: MainUserViewModel - override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSkillsBinding { - return FragmentSkillsBinding.inflate(inflater, container, false) + override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRecyclerviewBinding { + return FragmentRecyclerviewBinding.inflate(inflater, container, false) } override fun onCreateView( diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt index 5b138b3ac..1e18e3b43 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt @@ -5,6 +5,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Shapes import androidx.compose.material.Typography import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.TextStyle @@ -12,7 +13,10 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat import com.google.android.material.composethemeadapter.createMdcTheme +import com.habitrpg.android.habitica.R +import com.habitrpg.common.habitica.extensions.getThemeColor @Composable fun HabiticaTheme( @@ -113,6 +117,36 @@ val Typography.subtitle3 ) object HabiticaTheme { val typography: Typography - @Composable - get() = MaterialTheme.typography -} \ No newline at end of file + @Composable + get() = MaterialTheme.typography + + + val shapes: Shapes + @Composable + get() = MaterialTheme.shapes + + val colors: HabiticaColors + @Composable + get() { + val context = LocalContext.current + return HabiticaColors( + windowBackground = Color(context.getThemeColor(R.attr.colorWindowBackground)), + contentBackground = Color(context.getThemeColor(R.attr.colorContentBackground)), + contentBackgroundOffset = Color(context.getThemeColor(R.attr.colorContentBackgroundOffset)), + textPrimary = Color(context.getThemeColor(R.attr.textColorPrimary)), + textSecondary = Color(context.getThemeColor(R.attr.textColorSecondary)), + textTertiary = Color(context.getThemeColor(R.attr.colorTertiary)), + textDimmed = Color(ContextCompat.getColor(context, R.color.text_dimmed)), + ) + } +} + +class HabiticaColors( + val windowBackground: Color, + val contentBackground: Color, + val contentBackgroundOffset: Color, + val textPrimary: Color, + val textSecondary: Color, + val textTertiary: Color, + val textDimmed: Color +) \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt index b9e1b0460..451484c8b 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -32,7 +33,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.colorResource @@ -44,6 +44,7 @@ import androidx.compose.ui.unit.sp import androidx.core.os.bundleOf import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.helpers.MainNavigationController +import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel @@ -71,6 +72,7 @@ fun UserLevelText(user: User) { @Composable fun AppHeaderView( viewModel: MainUserViewModel, + onMemberRowClicked: (List) -> Unit ) { val user by viewModel.user.observeAsState(null) val teamPlan by viewModel.currentTeamPlan.collectAsState(null) @@ -82,6 +84,9 @@ fun AppHeaderView( Modifier .size(110.dp, 100.dp) .padding(end = 16.dp) + .clickable { + MainNavigationController.navigate(R.id.avatarOverviewFragment) + } ) Column(modifier = Modifier.height(100.dp)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) { @@ -112,6 +117,20 @@ fun AppHeaderView( value = user?.stats?.mp ?: 0.0, maxValue = user?.stats?.maxMP?.toDouble() ?: 0.0, displayCompact = teamPlan != null, + modifier = Modifier.weight(1f) + .clickable { + MainNavigationController.navigate(R.id.skillsFragment) + } + ) + } else if ((user?.stats?.lvl ?: 0) < 10) { + LabeledBar( + icon = HabiticaIconsHelper.imageOfMagic(), + label = stringResource(R.string.unlock_level, 10), + color = colorResource(R.color.mpColor), + value = 0.0, + maxValue = 1.0, + displayCompact = teamPlan != null, + disabled = true, modifier = Modifier.weight(1f) ) } @@ -162,10 +181,7 @@ fun AppHeaderView( colorResource(R.color.window_background) ) .clickable { - MainNavigationController.navigate( - R.id.guildFragment, - bundleOf("groupID" to teamPlan?.id) - ) + teamPlanMembers?.let { onMemberRowClicked(it) } } ) { for (member in teamPlanMembers?.filter { it.id != user?.id }?.take(6) ?: emptyList()) { @@ -185,25 +201,16 @@ fun AppHeaderView( } } } - Row(verticalAlignment = Alignment.CenterVertically) { - if (user?.hasClass == true) { - val icon = when (user?.stats?.habitClass) { - "warrior" -> HabiticaIconsHelper.imageOfWarriorLightBg().asImageBitmap() - "wizard" -> HabiticaIconsHelper.imageOfMageLightBg().asImageBitmap() - "healer" -> HabiticaIconsHelper.imageOfHealerLightBg().asImageBitmap() - "rogue" -> HabiticaIconsHelper.imageOfRogueLightBg().asImageBitmap() - else -> null - } - if (icon != null) { - Image(bitmap = icon, "", modifier = Modifier.padding(end = 4.dp)) - } - } + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.defaultMinSize(minHeight = 28.dp)) { + ClassIcon(className = user?.stats?.habitClass, hasClass = user?.hasClass ?: false, modifier = Modifier.padding(4.dp)) user?.let { UserLevelText(it) } Spacer(Modifier.weight(1f)) user?.hourglassCount?.toDouble() ?.let { CurrencyText("hourglasses", it, modifier = Modifier.padding(end = 12.dp)) } CurrencyText("gold", user?.stats?.gp ?: 0.0, modifier = Modifier.padding(end = 12.dp)) - CurrencyText("gems", user?.gemCount?.toDouble() ?: 0.0) + CurrencyText("gems", user?.gemCount?.toDouble() ?: 0.0, modifier = Modifier.clickable { + MainNavigationController.navigate(R.id.gemPurchaseActivity, bundleOf(Pair("openSubscription", false))) + }) } } } \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ClassIcon.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ClassIcon.kt new file mode 100644 index 000000000..8293820f8 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ClassIcon.kt @@ -0,0 +1,22 @@ +package com.habitrpg.android.habitica.ui.views + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap + +@Composable +fun ClassIcon(className: String?, hasClass: Boolean, modifier: Modifier = Modifier) { + if (hasClass) { + val icon = when (className) { + "warrior" -> HabiticaIconsHelper.imageOfWarriorLightBg().asImageBitmap() + "wizard" -> HabiticaIconsHelper.imageOfMageLightBg().asImageBitmap() + "healer" -> HabiticaIconsHelper.imageOfHealerLightBg().asImageBitmap() + "rogue" -> HabiticaIconsHelper.imageOfRogueLightBg().asImageBitmap() + else -> null + } + if (icon != null) { + Image(bitmap = icon, "", modifier = modifier) + } + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/EquipmentOverviewView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/EquipmentOverviewView.kt deleted file mode 100644 index 052718b98..000000000 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/EquipmentOverviewView.kt +++ /dev/null @@ -1,218 +0,0 @@ -package com.habitrpg.android.habitica.ui.views - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.models.user.Outfit -import com.habitrpg.android.habitica.models.user.Preferences -import com.habitrpg.android.habitica.ui.theme.HabiticaTheme -import com.habitrpg.android.habitica.ui.theme.caption2 - -@Composable -fun OverviewItem( - text: String, - iconName: String?, - modifier: Modifier = Modifier, - isTwoHanded: Boolean = false -) { - val hasIcon = iconName?.isNotBlank() == true - Column( - horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier - .width(70.dp) - ) { - Box( - Modifier - .size(70.dp) - .clip(MaterialTheme.shapes.small) - .background(colorResource(if (hasIcon) R.color.gray_700 else R.color.gray_10)), - contentAlignment = Alignment.Center - ) { - if (isTwoHanded) { - Image(painterResource(R.drawable.equipment_two_handed), null) - } else if (hasIcon) { - PixelArtView( - imageName = iconName, Modifier - .size(70.dp) - ) - } else { - Image(painterResource(R.drawable.equipment_nothing_equipped), null) - } - } - Text( - text, - style = HabiticaTheme.typography.caption2, - color = colorResource(R.color.gray_400), - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 4.dp) - ) - } -} - -@Composable -fun EquipmentOverviewView( - outfit: Outfit?, - onEquipmentTap: (String, String?) -> Unit, - modifier: Modifier = Modifier -) { - Column( - verticalArrangement = Arrangement.spacedBy(18.dp), - modifier = modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background(colorResource(R.color.gray_50)) - .padding(12.dp) - ) { - Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - OverviewItem(stringResource(R.string.outfit_weapon), outfit?.weapon.let { "shop_$it" }, Modifier.clickable { - onEquipmentTap("weapon", null) - }) - OverviewItem(stringResource(R.string.outfit_shield), outfit?.shield.let { "shop_$it" }, Modifier.clickable { - onEquipmentTap("shield", null) - }) - OverviewItem(stringResource(R.string.outfit_head), outfit?.head.let { "shop_$it" }, Modifier.clickable { - onEquipmentTap("head", null) - }) - OverviewItem(stringResource(R.string.outfit_armor), outfit?.armor.let { "shop_$it" }, Modifier.clickable { - onEquipmentTap("armor", null) - }) - } - Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - OverviewItem( - stringResource(R.string.outfit_headAccessory), - outfit?.headAccessory.let { "shop_$it" }, Modifier.clickable { - onEquipmentTap("headAccessory", null) - }) - OverviewItem(stringResource(R.string.outfit_body), outfit?.body.let { "shop_$it" }, Modifier.clickable { - onEquipmentTap("body", null) - }) - OverviewItem(stringResource(R.string.outfit_back), outfit?.back.let { "shop_$it" }, Modifier.clickable { - onEquipmentTap("back", null) - }) - OverviewItem( - stringResource(R.string.outfit_eyewear), - outfit?.eyeWear.let { "shop_$it" }, Modifier.clickable { - onEquipmentTap("eyewear", null) - }) - } - } -} - -@Composable -fun AvatarCustomizationOverviewView( - preferences: Preferences?, - onCustomizationTap: (String, String?) -> Unit, - modifier: Modifier = Modifier -) { - Column( - verticalArrangement = Arrangement.spacedBy(18.dp), - modifier = modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background(colorResource(R.color.gray_50)) - .padding(12.dp) - ) { - Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - OverviewItem( - stringResource(R.string.avatar_shirt), - preferences?.shirt.let { "${preferences?.size}_shirt$it" }, Modifier.clickable { - onCustomizationTap("shirt", null) - }) - OverviewItem( - stringResource(R.string.avatar_skin), - preferences?.skin.let { "skin_$it" }, - Modifier.clickable { - onCustomizationTap("skin", null) - }) - OverviewItem( - stringResource(R.string.avatar_hair_color), - if (preferences?.hair?.color != null && preferences.hair?.color != "") "hair_bangs_1_" + preferences.hair?.color else "", - Modifier.clickable { - onCustomizationTap("hair", "color") - } - ) - OverviewItem( - stringResource(R.string.avatar_hair_bangs), - if (preferences?.hair?.bangs != null && preferences.hair?.bangs != 0) "hair_bangs_" + preferences.hair?.bangs + "_" + preferences.hair?.color else "", - Modifier.clickable { - onCustomizationTap("hair", "bangs") - } - ) - } - Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - OverviewItem( - stringResource(R.string.avatar_style), - if (preferences?.hair?.base != null && preferences.hair?.base != 0) "hair_base_" + preferences.hair?.base + "_" + preferences.hair?.color else "", - Modifier.clickable { - onCustomizationTap("hair", "base") - } - ) - OverviewItem( - stringResource(R.string.avatar_mustache), - if (preferences?.hair?.mustache != null && preferences.hair?.mustache != 0) "hair_mustache_" + preferences.hair?.mustache + "_" + preferences.hair?.color else "", - Modifier.clickable { - onCustomizationTap("hair", "mustache") - } - ) - OverviewItem( - stringResource(R.string.avatar_beard), - if (preferences?.hair?.beard != null && preferences.hair?.beard != 0) "hair_beard_" + preferences.hair?.beard + "_" + preferences.hair?.color else "", - Modifier.clickable { - onCustomizationTap("hair", "beard") - } - ) - OverviewItem( - stringResource(R.string.avatar_flower), - if (preferences?.hair?.flower != null && preferences.hair?.flower != 0) "hair_flower_" + preferences.hair?.flower else "", - Modifier.clickable { - onCustomizationTap("hair", "flower") - } - ) - } - Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - OverviewItem( - stringResource(R.string.avatar_wheelchair), - preferences?.chair?.let { if (it.startsWith("handleless")) "chair_$it" else it }) - OverviewItem( - stringResource(R.string.avatar_background), - preferences?.background.let { "background_$it" }) - Box(Modifier.size(70.dp)) - Box(Modifier.size(70.dp)) - } - } -} - -@Preview -@Composable -fun EquipmentOverviewItemPreview() { - Column(Modifier.width(320.dp)) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OverviewItem("Main-Hand", "shop_weapon_warrior_1") - OverviewItem("Off-Hand", null, isTwoHanded = true) - OverviewItem("Armor", null) - } - EquipmentOverviewView(null, { _, _ -> }) - AvatarCustomizationOverviewView(null, { _, _ -> }) - } -} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/GroupPlanMemberList.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/GroupPlanMemberList.kt new file mode 100644 index 000000000..034e03f6b --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/GroupPlanMemberList.kt @@ -0,0 +1,201 @@ +package com.habitrpg.android.habitica.ui.views + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.models.auth.LocalAuthentication +import com.habitrpg.android.habitica.models.members.Member +import com.habitrpg.android.habitica.models.user.Authentication +import com.habitrpg.android.habitica.models.user.Profile +import com.habitrpg.android.habitica.models.user.Stats +import com.habitrpg.android.habitica.ui.theme.HabiticaTheme +import com.habitrpg.common.habitica.extensions.getThemeColor +import kotlin.random.Random + +@Composable +fun GroupPlanMemberList( + members: List, + onMemberClicked: (String) -> Unit, + onMoreClicked: (Member) -> Unit +) { + LazyColumn { + for (member in members) { + item { + MemberItem( + member, + onMemberClicked, + onMoreClicked, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } + } +} + +@Composable +fun MemberItem( + member: Member, + onMemberClicked: (String) -> Unit, + onMoreClicked: (Member) -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier + .fillMaxWidth() + .clip(HabiticaTheme.shapes.large) + .background(HabiticaTheme.colors.windowBackground) + .clickable { + member.id?.let { onMemberClicked(it) } + } + ) { + TextButton( + onClick = { onMoreClicked(member) }, modifier = Modifier + .size(32.dp) + .background( + Color(LocalContext.current.getThemeColor(R.attr.colorAccent)), + WobblyCircle + ) + .align(Alignment.TopEnd) + ) { + Image(painterResource(R.drawable.menu_messages), null) + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(14.dp) + ) { + ComposableAvatarView(avatar = member, modifier = Modifier.size(94.dp, 98.dp)) + Column(verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier.height(100.dp)) { + Text( + member.displayName, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + color = HabiticaTheme.colors.textPrimary + ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + member.formattedUsername ?: "", + color = HabiticaTheme.colors.textSecondary + ) + Spacer( + Modifier + .weight(1.0f) + .background(Color.Red) + ) + ClassIcon( + member.stats?.habitClass, + member.hasClass, + modifier = Modifier.size(18.dp) + ) + BuffIcon(member.stats?.isBuffed) + CurrencyText(currency = "gold", value = member.stats?.gp ?: 0.0) + } + LabeledBar( + color = colorResource(R.color.hpColor), + barColor = HabiticaTheme.colors.contentBackgroundOffset, + value = member.stats?.hp ?: 0.0, + maxValue = (member.stats?.maxHealth ?: 0).toDouble(), + displayCompact = true + ) + LabeledBar( + color = colorResource(R.color.xpColor), + barColor = HabiticaTheme.colors.contentBackgroundOffset, + value = member.stats?.exp ?: 0.0, + maxValue = (member.stats?.toNextLevel ?: 0).toDouble(), + displayCompact = true + ) + if (member.hasClass) { + LabeledBar( + color = colorResource(R.color.mpColor), + barColor = HabiticaTheme.colors.contentBackgroundOffset, + value = member.stats?.mp ?: 0.0, + maxValue = (member.stats?.maxMP ?: 0).toDouble(), + displayCompact = true + ) + } + Row(horizontalArrangement = Arrangement.SpaceBetween) { + Text( + stringResource(R.string.level_unabbreviated, member.stats?.lvl ?: 0), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = HabiticaTheme.colors.textPrimary + ) + Text( + "", fontWeight = FontWeight.SemiBold, fontSize = 14.sp, + color = HabiticaTheme.colors.textPrimary + ) + } + } + } + } +} + +@Composable +fun BuffIcon(buffed: Boolean?, modifier: Modifier = Modifier) { + if (buffed == true) { + Image(HabiticaIconsHelper.imageOfBuffIcon().asImageBitmap(), null, modifier = modifier) + } +} + +private class MemberProvider : PreviewParameterProvider { + override val values: Sequence + get() { + val list = mutableListOf() + for (x in 0..5) { + val member = Member() + member.profile = Profile() + member.profile?.name = "User $x" + member.authentication = Authentication() + member.authentication?.localAuthentication = LocalAuthentication() + member.authentication?.localAuthentication?.username = "user${x}" + member.stats = Stats() + member.stats?.hp = Random.nextDouble() + member.stats?.maxHealth = 50 + member.stats?.toNextLevel = Random.nextInt() + member.stats?.exp = + Random.nextDouble(until = (member.stats?.toNextLevel ?: 0).toDouble()) + member.stats?.maxMP = Random.nextInt() + member.stats?.mp = Random.nextDouble(until = (member.stats?.maxMP ?: 0).toDouble()) + member.stats?.lvl = Random.nextInt() + list.add(member) + } + return list.asSequence() + } +} + +@Composable +@Preview +private fun Preview(@PreviewParameter(MemberProvider::class) member: Member) { + MemberItem(member = member, onMemberClicked = {}, onMoreClicked = {}) +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LabeledBar.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LabeledBar.kt index 7d05e9446..cd7179d3f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LabeledBar.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LabeledBar.kt @@ -6,12 +6,14 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.ProgressIndicatorDefaults @@ -19,43 +21,50 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.habitrpg.android.habitica.R -import java.math.RoundingMode -import java.text.NumberFormat +import com.habitrpg.android.habitica.ui.theme.HabiticaTheme +import com.habitrpg.common.habitica.helpers.NumberAbbreviator @Composable fun LabeledBar( - icon: Bitmap, - label: String, - color: Color, + modifier: Modifier = Modifier, + icon: Bitmap? = null, + label: String? = null, + color: Color = colorResource(R.color.brand), + barColor: Color = HabiticaTheme.colors.windowBackground, value: Double, maxValue: Double, displayCompact: Boolean, - modifier: Modifier = Modifier + disabled: Boolean = false ) { - val formatter = NumberFormat.getInstance() - formatter.maximumFractionDigits = 1 - formatter.roundingMode = RoundingMode.UP - formatter.isGroupingUsed = true - val cleanedMaxVlaue = java.lang.Double.max(1.0, maxValue) val animatedValue = animateFloatAsState( targetValue = value.toFloat(), animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, ).value - Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { - AnimatedVisibility( - visible = !displayCompact, - enter = slideInHorizontally { -18 }, - exit = slideOutHorizontally { -18 }) { - Image(icon.asImageBitmap(), null, modifier = Modifier.padding(end = 8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.alpha(if (disabled) 0.5f else 1.0f) + ) { + icon?.let { + AnimatedVisibility(visible = !displayCompact, + enter = slideInHorizontally { -18 }, + exit = slideOutHorizontally { -18 }) { + Image( + it.asImageBitmap(), null, modifier = Modifier.padding(end = 8.dp) + ) + } } Column(modifier = Modifier.weight(1f)) { LinearProgressIndicator( @@ -64,7 +73,7 @@ fun LabeledBar( .fillMaxWidth() .clip(CircleShape) .height(8.dp), - backgroundColor = colorResource(R.color.window_background), + backgroundColor = barColor, color = color ) AnimatedVisibility(visible = !displayCompact) { @@ -72,15 +81,51 @@ fun LabeledBar( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 2.dp) ) { - Text( - "${formatter.format(animatedValue)} / ${formatter.format(cleanedMaxVlaue)}", - fontSize = 12.sp, - color = colorResource(R.color.text_ternary) - ) + if (!disabled) { + Text( + "${NumberAbbreviator.abbreviate(LocalContext.current, animatedValue)} / ${NumberAbbreviator.abbreviate(LocalContext.current, cleanedMaxVlaue)}", + fontSize = 12.sp, + color = colorResource(R.color.text_ternary) + ) + } Spacer(Modifier.weight(1f)) - Text(label, fontSize = 12.sp, color = colorResource(R.color.text_ternary)) + if (label != null) { + Text(label, fontSize = 12.sp, color = colorResource(R.color.text_ternary)) + } } } } } +} + +@Composable +@Preview +private fun Preview() { + Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.width(180.dp)) { + LabeledBar( + icon = HabiticaIconsHelper.imageOfHeartLightBg(), + label = stringResource(id = R.string.health), + color = colorResource(R.color.hpColor), + value = 10.0, + maxValue = 50.0, + displayCompact = false + ) + LabeledBar( + icon = HabiticaIconsHelper.imageOfExperience(), + label = stringResource(id = R.string.XP_default), + color = colorResource(R.color.xpColor), + value = 100123.0, + maxValue = 50000000000000.0, + displayCompact = false + ) + LabeledBar( + icon = HabiticaIconsHelper.imageOfMagic(), + label = stringResource(id = R.string.unlock_level, 10), + color = colorResource(R.color.mpColor), + value = 10.0, + maxValue = 5000.0, + displayCompact = false, + disabled = true + ) + } } \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/WobblyCircle.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/WobblyCircle.kt new file mode 100644 index 000000000..27e3e3e1e --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/WobblyCircle.kt @@ -0,0 +1,23 @@ +package com.habitrpg.android.habitica.ui.views + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +object WobblyCircle : Shape { + override fun createOutline( + size: Size, layoutDirection: LayoutDirection, density: Density + ): Outline { + val path = Path().apply { + addOval(Rect(Offset(0f, -size.height / 0.4f), + Size(size.width * 1.8f, size.height * 1.6f))) + } + + return Outline.Generic(path) + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/EquipmentItemRow.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentItemRow.kt similarity index 96% rename from Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/EquipmentItemRow.kt rename to Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentItemRow.kt index 57612a492..bac0cd229 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/EquipmentItemRow.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentItemRow.kt @@ -1,4 +1,4 @@ -package com.habitrpg.android.habitica.ui.views +package com.habitrpg.android.habitica.ui.views.equipment import android.content.Context import android.util.AttributeSet diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentOverviewItem.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentOverviewItem.kt deleted file mode 100644 index 5c722cc5d..000000000 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentOverviewItem.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.habitrpg.android.habitica.ui.views.equipment - -import android.content.Context -import android.graphics.drawable.BitmapDrawable -import android.util.AttributeSet -import android.view.View -import android.widget.LinearLayout -import androidx.core.content.ContextCompat -import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.databinding.EquipmentOverviewItemBinding -import com.habitrpg.common.habitica.extensions.layoutInflater -import com.habitrpg.common.habitica.extensions.loadImage -import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper - -class EquipmentOverviewItem @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr) { - - private var binding: EquipmentOverviewItemBinding = EquipmentOverviewItemBinding.inflate(context.layoutInflater, this) - - init { - if (attrs != null) { - val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.EquipmentOverviewItem) - binding.titleView.text = styledAttrs.getString(R.styleable.EquipmentOverviewItem_title) - styledAttrs.recycle() - } - orientation = VERTICAL - } - - var identifier: String = "" - - fun set(key: String?, isTwoHanded: Boolean = false, isDisabledFromTwoHand: Boolean = false) { - identifier = key ?: "" - binding.twoHandedIndicator.setImageDrawable(null) - if (identifier.isNotEmpty() && !identifier.endsWith("base_0")) { - binding.iconView.loadImage("shop_$key") - binding.localIconView.visibility = View.GONE - binding.iconView.visibility = View.VISIBLE - binding.iconWrapper.background = ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg_content) - if (isTwoHanded) { - binding.twoHandedIndicator.setImageDrawable(BitmapDrawable(context.resources, HabiticaIconsHelper.imageOfTwoHandedIcon())) - } - } else { - binding.localIconView.visibility = View.VISIBLE - binding.iconView.visibility = View.GONE - if (isDisabledFromTwoHand) { - binding.iconWrapper.background = ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg_content) - binding.localIconView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.equipment_two_handed)) - } else { - binding.iconWrapper.background = ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg_gray_10) - binding.localIconView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.equipment_nothing_equipped)) - } - } - } -} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentOverviewView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentOverviewView.kt index 9d9a3ba0b..2928f2560 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentOverviewView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/equipment/EquipmentOverviewView.kt @@ -1,47 +1,217 @@ -package com.habitrpg.android.habitica.ui.views.equipment +package com.habitrpg.android.habitica.ui.views -import android.content.Context -import android.util.AttributeSet -import android.widget.LinearLayout -import androidx.core.content.ContextCompat +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.databinding.EquipmentOverviewViewBinding -import com.habitrpg.common.habitica.extensions.layoutInflater -import com.habitrpg.android.habitica.extensions.setScaledPadding import com.habitrpg.android.habitica.models.user.Outfit +import com.habitrpg.android.habitica.models.user.Preferences +import com.habitrpg.android.habitica.ui.theme.HabiticaTheme +import com.habitrpg.android.habitica.ui.theme.caption2 -class EquipmentOverviewView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr) { - - var onNavigate: ((String, String) -> Unit)? = null - private var binding: EquipmentOverviewViewBinding = EquipmentOverviewViewBinding.inflate(context.layoutInflater, this) - - init { - background = ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg_gray_50) - setScaledPadding(context, 12, 12, 12, 12) - orientation = VERTICAL - - binding.weaponItem.setOnClickListener { onNavigate?.invoke("weapon", binding.weaponItem.identifier) } - binding.shieldItem.setOnClickListener { onNavigate?.invoke("shield", binding.shieldItem.identifier) } - binding.headItem.setOnClickListener { onNavigate?.invoke("head", binding.headItem.identifier) } - binding.armorItem.setOnClickListener { onNavigate?.invoke("armor", binding.armorItem.identifier) } - binding.headAccessoryItem.setOnClickListener { onNavigate?.invoke("headAccessory", binding.headAccessoryItem.identifier) } - binding.bodyItem.setOnClickListener { onNavigate?.invoke("body", binding.bodyItem.identifier) } - binding.backItem.setOnClickListener { onNavigate?.invoke("back", binding.backItem.identifier) } - binding.eyewearItem.setOnClickListener { onNavigate?.invoke("eyewear", binding.eyewearItem.identifier) } - } - - fun updateData(outfit: Outfit?, isWeaponTwoHanded: Boolean = false) { - binding.weaponItem.set(outfit?.weapon, isWeaponTwoHanded) - binding.shieldItem.set(outfit?.shield, false, isWeaponTwoHanded) - binding.headItem.set(outfit?.head) - binding.armorItem.set(outfit?.armor) - binding.headAccessoryItem.set(outfit?.headAccessory) - binding.bodyItem.set(outfit?.body) - binding.backItem.set(outfit?.back) - binding.eyewearItem.set(outfit?.eyeWear) +@Composable +fun OverviewItem( + text: String, + iconName: String?, + modifier: Modifier = Modifier, + isTwoHanded: Boolean = false +) { + val hasIcon = iconName?.isNotBlank() == true + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier + .width(70.dp) + ) { + Box( + Modifier + .size(70.dp) + .clip(MaterialTheme.shapes.small) + .background(colorResource(if (hasIcon) R.color.gray_700 else R.color.gray_10)), + contentAlignment = Alignment.Center + ) { + if (isTwoHanded) { + Image(painterResource(R.drawable.equipment_two_handed), null) + } else if (hasIcon) { + PixelArtView( + imageName = iconName, Modifier + .size(70.dp) + ) + } else { + Image(painterResource(R.drawable.equipment_nothing_equipped), null) + } + } + Text( + text, + style = HabiticaTheme.typography.caption2, + color = colorResource(R.color.gray_400), + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 4.dp) + ) } } + +@Composable +fun EquipmentOverviewView( + outfit: Outfit?, + onEquipmentTap: (String, String?) -> Unit, + modifier: Modifier = Modifier +) { + Column( + verticalArrangement = Arrangement.spacedBy(18.dp), + modifier = modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(colorResource(R.color.gray_50)) + .padding(12.dp) + ) { + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + OverviewItem(stringResource(R.string.outfit_weapon), outfit?.weapon.let { "shop_$it" }, Modifier.clickable { + onEquipmentTap("weapon", null) + }) + OverviewItem(stringResource(R.string.outfit_shield), outfit?.shield.let { "shop_$it" }, Modifier.clickable { + onEquipmentTap("shield", null) + }) + OverviewItem(stringResource(R.string.outfit_head), outfit?.head.let { "shop_$it" }, Modifier.clickable { + onEquipmentTap("head", null) + }) + OverviewItem(stringResource(R.string.outfit_armor), outfit?.armor.let { "shop_$it" }, Modifier.clickable { + onEquipmentTap("armor", null) + }) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + OverviewItem( + stringResource(R.string.outfit_headAccessory), + outfit?.headAccessory.let { "shop_$it" }, Modifier.clickable { + onEquipmentTap("headAccessory", null) + }) + OverviewItem(stringResource(R.string.outfit_body), outfit?.body.let { "shop_$it" }, Modifier.clickable { + onEquipmentTap("body", null) + }) + OverviewItem(stringResource(R.string.outfit_back), outfit?.back.let { "shop_$it" }, Modifier.clickable { + onEquipmentTap("back", null) + }) + OverviewItem( + stringResource(R.string.outfit_eyewear), + outfit?.eyeWear.let { "shop_$it" }, Modifier.clickable { + onEquipmentTap("eyewear", null) + }) + } + } +} + +@Composable +fun AvatarCustomizationOverviewView( + preferences: Preferences?, + onCustomizationTap: (String, String?) -> Unit, + modifier: Modifier = Modifier +) { + Column( + verticalArrangement = Arrangement.spacedBy(18.dp), + modifier = modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(colorResource(R.color.gray_50)) + .padding(12.dp) + ) { + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + OverviewItem( + stringResource(R.string.avatar_shirt), + preferences?.shirt.let { "${preferences?.size}_shirt$it" }, Modifier.clickable { + onCustomizationTap("shirt", null) + }) + OverviewItem( + stringResource(R.string.avatar_skin), + preferences?.skin.let { "skin_$it" }, + Modifier.clickable { + onCustomizationTap("skin", null) + }) + OverviewItem( + stringResource(R.string.avatar_hair_color), + if (preferences?.hair?.color != null && preferences.hair?.color != "") "hair_bangs_1_" + preferences.hair?.color else "", + Modifier.clickable { + onCustomizationTap("hair", "color") + } + ) + OverviewItem( + stringResource(R.string.avatar_hair_bangs), + if (preferences?.hair?.bangs != null && preferences.hair?.bangs != 0) "hair_bangs_" + preferences.hair?.bangs + "_" + preferences.hair?.color else "", + Modifier.clickable { + onCustomizationTap("hair", "bangs") + } + ) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + OverviewItem( + stringResource(R.string.avatar_style), + if (preferences?.hair?.base != null && preferences.hair?.base != 0) "hair_base_" + preferences.hair?.base + "_" + preferences.hair?.color else "", + Modifier.clickable { + onCustomizationTap("hair", "base") + } + ) + OverviewItem( + stringResource(R.string.avatar_mustache), + if (preferences?.hair?.mustache != null && preferences.hair?.mustache != 0) "hair_mustache_" + preferences.hair?.mustache + "_" + preferences.hair?.color else "", + Modifier.clickable { + onCustomizationTap("hair", "mustache") + } + ) + OverviewItem( + stringResource(R.string.avatar_beard), + if (preferences?.hair?.beard != null && preferences.hair?.beard != 0) "hair_beard_" + preferences.hair?.beard + "_" + preferences.hair?.color else "", + Modifier.clickable { + onCustomizationTap("hair", "beard") + } + ) + OverviewItem( + stringResource(R.string.avatar_flower), + if (preferences?.hair?.flower != null && preferences.hair?.flower != 0) "hair_flower_" + preferences.hair?.flower else "", + Modifier.clickable { + onCustomizationTap("hair", "flower") + } + ) + } + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + OverviewItem( + stringResource(R.string.avatar_wheelchair), + preferences?.chair?.let { if (it.startsWith("handleless")) "chair_$it" else it }) + OverviewItem( + stringResource(R.string.avatar_background), + preferences?.background.let { "background_$it" }) + Box(Modifier.size(70.dp)) + Box(Modifier.size(70.dp)) + } + } +} + +@Preview +@Composable +fun EquipmentOverviewItemPreview() { + Column(Modifier.width(320.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OverviewItem("Main-Hand", "shop_weapon_warrior_1") + OverviewItem("Off-Hand", null, isTwoHanded = true) + OverviewItem("Armor", null) + } + EquipmentOverviewView(null, { _, _ -> }) + AvatarCustomizationOverviewView(null, { _, _ -> }) + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/AssignSheet.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/AssignSheet.kt new file mode 100644 index 000000000..c8d2d6c58 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/AssignSheet.kt @@ -0,0 +1,132 @@ +package com.habitrpg.android.habitica.ui.views.tasks + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.models.members.Member +import com.habitrpg.android.habitica.ui.views.UserRow + +@Composable +fun AssignSheet( + members: List, + assignedMembers: List, + onAssignClick: (String) -> Unit, + onCloseClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier) { + Box { + Text( + stringResource(R.string.assign_to), + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = colorResource(R.color.gray_200), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) + TextButton( + onClick = onCloseClick, + colors = ButtonDefaults.textButtonColors(), + modifier = Modifier.align(Alignment.CenterEnd) + ) { + Text(stringResource(R.string.done)) + } + } + for (member in members) { + val isAssigned = assignedMembers.contains(member.id) + val transition = updateTransition(isAssigned, label = "isAssigned") + val rotation = transition.animateFloat( + label = "isAssigned", + transitionSpec = { spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow) }) { + if (it) 0f else 45f + } + val backgroundColor = transition.animateColor( + label = "isAssigned", + transitionSpec = { tween(400, easing = FastOutLinearInEasing) }) { + if (it) MaterialTheme.colors.primary else colorResource(id = R.color.transparent) + } + val color = transition.animateColor( + label = "isAssigned", + transitionSpec = { tween(400, easing = FastOutLinearInEasing) }) { + fadeIn(tween(10000)) + colorResource(if (it) R.color.white else R.color.text_dimmed) + } + val borderColor = transition.animateColor( + label = "isAssigned", + transitionSpec = { tween(400, easing = FastOutLinearInEasing) }) { + fadeIn(tween(10000)) + if (it) MaterialTheme.colors.primary else colorResource(id = R.color.text_dimmed) + } + UserRow( + username = member.displayName, + color = colorResource(R.color.text_primary), + extraContent = { + Text( + member.formattedUsername ?: "", + color = colorResource(R.color.text_ternary) + ) + }, endContent = { + Image( + painterResource(R.drawable.ic_close_white_24dp), + null, + colorFilter = ColorFilter.tint(color.value), + modifier = Modifier + .rotate(rotation.value) + .size(24.dp) + .background( + backgroundColor.value, + CircleShape + ) + .border( + 2.dp, + borderColor.value, + CircleShape + ) + .padding(3.dp) + ) + }, modifier = Modifier + .clickable { + member.id?.let { onAssignClick(it) } + } + .padding(30.dp, 12.dp) + .heightIn(min = 24.dp) + .fillMaxWidth() + ) + } + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/AssignedView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/AssignedView.kt new file mode 100644 index 000000000..0e0669568 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/AssignedView.kt @@ -0,0 +1,81 @@ +package com.habitrpg.android.habitica.ui.views.tasks + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.models.Assignable +import com.habitrpg.android.habitica.ui.views.CompletedAt +import com.habitrpg.android.habitica.ui.views.UserRow +import java.util.Date + +@Composable +fun AssignedView( + assigned: List, + completedAt: Map, + backgroundColor: Color, + color: Color, + onEditClick: () -> Unit, + modifier: Modifier = Modifier, + showEditButton: Boolean = false +) { + Column(modifier.fillMaxWidth()) { + val rowModifier = Modifier + .padding(vertical = 4.dp) + .background( + backgroundColor, + MaterialTheme.shapes.medium + ) + .padding(15.dp, 12.dp) + .heightIn(min = 24.dp) + .fillMaxWidth() + for (assignable in assigned) { + UserRow( + username = assignable.identifiableName, modifier = rowModifier, + color = color, + extraContent = { + completedAt[assignable.id]?.let { CompletedAt(completedAt = it) } + } + ) + } + if (showEditButton) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier + .clickable { + onEditClick() + } + .padding(vertical = 4.dp) + .background( + backgroundColor, + MaterialTheme.shapes.medium + ) + .padding(15.dp, 12.dp) + .heightIn(min = 24.dp) + .fillMaxWidth()) { + Image( + painterResource(R.drawable.edit), + null, + colorFilter = ColorFilter.tint(MaterialTheme.colors.primary) + ) + Text( + stringResource(R.string.edit_assignees), color = color, + modifier = Modifier.padding(start = 4.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/habitrpg/common/habitica/helpers/NumberAbbreviator.kt b/common/src/main/java/com/habitrpg/common/habitica/helpers/NumberAbbreviator.kt index 11e29dfe4..02b0b7feb 100644 --- a/common/src/main/java/com/habitrpg/common/habitica/helpers/NumberAbbreviator.kt +++ b/common/src/main/java/com/habitrpg/common/habitica/helpers/NumberAbbreviator.kt @@ -7,6 +7,10 @@ import java.text.DecimalFormat object NumberAbbreviator { + fun abbreviate(context: Context?, number: Float, numberOfDecimals: Int = 2, minForAbbrevation: Int = 0): String { + return abbreviate(context, number.toDouble(), numberOfDecimals, minForAbbrevation) + } + fun abbreviate(context: Context?, number: Double, numberOfDecimals: Int = 2, minForAbbrevation: Int = 0): String { var usedNumber = number var counter = 0 @@ -28,6 +32,7 @@ object NumberAbbreviator { 2 -> context?.getString(R.string.million_abbrev) ?: "m" 3 -> context?.getString(R.string.billion_abbrev) ?: "b" 4 -> context?.getString(R.string.trillion_abbrev) ?: "t" + 5 -> context?.getString(R.string.quadrillion_abbrev) ?: "q" else -> "" } } diff --git a/common/src/main/java/com/habitrpg/common/habitica/views/ValueBar.kt b/common/src/main/java/com/habitrpg/common/habitica/views/ValueBar.kt index 2e3a4a87b..d1c739c66 100644 --- a/common/src/main/java/com/habitrpg/common/habitica/views/ValueBar.kt +++ b/common/src/main/java/com/habitrpg/common/habitica/views/ValueBar.kt @@ -7,13 +7,10 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View import android.widget.FrameLayout -import androidx.core.content.ContextCompat -import com.habitrpg.common.habitica.extensions.dpToPx -import com.habitrpg.common.habitica.extensions.getThemeColor -import com.habitrpg.common.habitica.extensions.isUsingNightModeResources -import com.habitrpg.common.habitica.extensions.layoutInflater import com.habitrpg.common.habitica.R import com.habitrpg.common.habitica.databinding.ValueBarBinding +import com.habitrpg.common.habitica.extensions.dpToPx +import com.habitrpg.common.habitica.extensions.layoutInflater import java.math.RoundingMode import java.text.NumberFormat @@ -95,7 +92,6 @@ class ValueBar(context: Context, attrs: AttributeSet?) : FrameLayout(context, at R.styleable.ValueBar, 0, 0 ) - setLightBackground(attributes?.getBoolean(R.styleable.ValueBar_lightBackground, !context.isUsingNightModeResources()) == true) binding.progressBar.barForegroundColor = attributes?.getColor(R.styleable.ValueBar_barForegroundColor, 0) ?: 0 binding.progressBar.barPendingColor = attributes?.getColor(R.styleable.ValueBar_barPendingColor, 0) ?: 0 @@ -163,19 +159,6 @@ class ValueBar(context: Context, attrs: AttributeSet?) : FrameLayout(context, at binding.valueTextView.text = valueText } - fun setLightBackground(lightBackground: Boolean) { - val textColor: Int - /*if (lightBackground) { - textColor = ContextCompat.getColor(context, R.color.text_ternary) - binding.progressBar.setBackgroundResource(R.drawable.layout_rounded_bg_light_gray) - } else { - textColor = context.getThemeColor(R.attr.textColorPrimaryDark) - binding.progressBar.setBackgroundResource(R.drawable.layout_rounded_bg_header_bar) - } - binding.valueTextView.setTextColor(textColor) - binding.descriptionTextView.setTextColor(textColor)*/ - } - var animationDuration = 500L var animationDelay = 0L diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 327b79e24..f41d9112f 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ m b t + q Gems gold Gold diff --git a/shared/src/commonMain/kotlin/com/habitrpg/shared/habitica/models/Avatar.kt b/shared/src/commonMain/kotlin/com/habitrpg/shared/habitica/models/Avatar.kt index 7b30af6dd..6b2f43fab 100644 --- a/shared/src/commonMain/kotlin/com/habitrpg/shared/habitica/models/Avatar.kt +++ b/shared/src/commonMain/kotlin/com/habitrpg/shared/habitica/models/Avatar.kt @@ -27,12 +27,14 @@ interface Avatar { val hasClass: Boolean get() { - return preferences?.disableClasses != true && flags?.classSelected ?: true == true && stats?.habitClass?.isNotEmpty() == true + return preferences?.disableClasses != true + && flags?.classSelected != false + && stats?.habitClass?.isNotEmpty() == true + && (stats?.lvl ?: 0) >= 10 } val currentMount: String? get() = items?.currentMount ?: "" - val currentPet: String? get() = items?.currentPet ?: "" diff --git a/version.properties b/version.properties index eca0c163d..1dc1c6bf1 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ NAME=4.1 -CODE=4771 \ No newline at end of file +CODE=4781 \ No newline at end of file