From 49209c4c4a3c5a7838c151fa1751b74ec1528e39 Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 7 Mar 2024 06:31:25 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=20-=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=ED=8C=90=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/LiveRoomActivity.kt | 63 ++--- .../sodalive/live/room/LiveRoomViewModel.kt | 63 ++++- .../room/create/LiveRoomCreateActivity.kt | 12 +- .../room/create/LiveRoomCreateViewModel.kt | 6 +- ...nuResponse.kt => GetMenuPresetResponse.kt} | 5 +- .../sodalive/live/room/menu/MenuApi.kt | 2 +- .../room/update/EditLiveRoomInfoRequest.kt | 5 +- .../room/update/LiveRoomInfoEditDialog.kt | 215 +++++++++++++++++- .../layout/dialog_live_room_info_update.xml | 163 +++++++++++++ 9 files changed, 482 insertions(+), 52 deletions(-) rename app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/{GetMenuResponse.kt => GetMenuPresetResponse.kt} (51%) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt index 18b5561..dcca8c1 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt @@ -29,7 +29,6 @@ import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import coil.load import coil.transform.CircleCropTransformation import com.github.dhaval2404.imagepicker.ImagePicker import com.google.gson.Gson @@ -696,37 +695,44 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB if (response.creatorId == SharedPreferenceManager.userId) { binding.ivEdit.setOnClickListener { - roomInfoEditDialog.setRoomInfo(response.title, response.notice) - roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl) - roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageUri -> - viewModel.editLiveRoomInfo( - response.roomId, - newTitle, - newContent, - newCoverImageUri, - onSuccess = { - binding.tvTitle.text = newTitle - setNoticeAndClickableUrl(binding.tvNotice, newContent) + viewModel.getAllMenuPreset { + roomInfoEditDialog.setRoomInfo(response.title, response.notice) + roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl) + roomInfoEditDialog.setMenuPreset(it) + roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageUri, isActivateMenu, menuId, menu -> + viewModel.editLiveRoomInfo( + response.roomId, + newTitle, + newContent, + newCoverImageUri, + isActivateMenu, + menuId, + menu, + onSuccess = { + Toast.makeText( + applicationContext, + "라이브 정보가 수정되었습니다.", + Toast.LENGTH_LONG + ).show() - if (newCoverImageUri != null) { - binding.ivCover.load(newCoverImageUri) + agora.sendRawMessageToGroup( + rawMessage = Gson().toJson( + LiveRoomChatRawMessage( + type = LiveRoomChatRawMessageType.EDIT_ROOM_INFO, + message = "", + can = 0, + donationMessage = "" + ) + ).toByteArray() + ) } + ) + } - agora.sendRawMessageToGroup( - rawMessage = Gson().toJson( - LiveRoomChatRawMessage( - type = LiveRoomChatRawMessageType.EDIT_ROOM_INFO, - message = "", - can = 0, - donationMessage = "" - ) - ).toByteArray() - ) - } - ) + handler.post { + roomInfoEditDialog.show(screenWidth) + } } - - roomInfoEditDialog.show(screenWidth) } binding.ivEdit.visibility = View.VISIBLE @@ -811,6 +817,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB binding.tvMenuPan.visibility = View.VISIBLE binding.tvMenuPanDetail.text = response.menuPan } else { + viewModel.toggleShowMenuPan(false) binding.tvMenuPan.visibility = View.GONE binding.tvMenuPanDetail.text = "" } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt index 755fc26..06612bf 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt @@ -18,6 +18,7 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.live.LiveRepository import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse +import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse import kr.co.vividnext.sodalive.live.room.update.EditLiveRoomInfoRequest import kr.co.vividnext.sodalive.live.roulette.RouletteItem @@ -363,9 +364,14 @@ class LiveRoomViewModel( _isShowNotice.value = !isShowNotice.value!! } - fun toggleShowMenuPan() { + fun toggleShowMenuPan(isShowMenuPan: Boolean? = null) { _isShowNotice.value = false - _isShowMenuPan.value = !isShowMenuPan.value!! + + if (isShowMenuPan != null) { + _isShowMenuPan.value = isShowMenuPan + } else { + _isShowMenuPan.value = !this.isShowMenuPan.value!! + } } fun toggleBackgroundImage() { @@ -377,6 +383,9 @@ class LiveRoomViewModel( newTitle: String, newContent: String, newCoverImageUri: Uri? = null, + isActivateMenu: Boolean?, + menuId: Long, + menu: String, onSuccess: () -> Unit ) { val request = EditLiveRoomInfoRequest( @@ -392,17 +401,25 @@ class LiveRoomViewModel( }, numberOfPeople = null, beginDateTimeString = null, - timezone = null + timezone = null, + menuPanId = if (isActivateMenu == true) menuId else 0, + menuPan = if (isActivateMenu == true) menu else "", + isActiveMenuPan = isActivateMenu ) - val requestJson = if (request.title != null || request.notice != null) { + val requestJson = if ( + request.title != null || + request.notice != null || + request.isActiveMenuPan != null || + request.menuPan.isNotBlank() + ) { Gson().toJson(request) } else { null } val coverImage = if (newCoverImageUri != null) { - val file = File(getRealPathFromURI(newCoverImageUri!!)) + val file = File(getRealPathFromURI(newCoverImageUri)) MultipartBody.Part.createFormData( "coverImage", file.name, @@ -637,7 +654,7 @@ class LiveRoomViewModel( { _isLoading.value = false if (it.success && it.data != null) { - _userProfileLiveData.value = it.data!! + _userProfileLiveData.value = it.data onSuccess() } else { if (it.message != null) { @@ -910,6 +927,40 @@ class LiveRoomViewModel( ) } + fun getAllMenuPreset(onSuccess: (List) -> Unit) { + _isLoading.value = true + + compositeDisposable.add( + repository.getAllMenu( + creatorId = SharedPreferenceManager.userId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + onSuccess(it.data ?: listOf()) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + private fun randomSelectRouletteItem( can: Int, items: List, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt index 337210c..c066643 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt @@ -731,13 +731,13 @@ class LiveRoomCreateActivity : BaseActivity( } private fun selectMenuPresetButton( - ivSelectRoulette: ImageView, - llSelectRoulette: LinearLayout, - tvSelectRoulette: TextView + ivSelectMenuPreset: ImageView, + llSelectMenuPreset: LinearLayout, + tvSelectMenuPreset: TextView ) { - ivSelectRoulette.visibility = View.VISIBLE - llSelectRoulette.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1) - tvSelectRoulette.setTextColor( + ivSelectMenuPreset.visibility = View.VISIBLE + llSelectMenuPreset.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1) + tvSelectMenuPreset.setTextColor( ContextCompat.getColor( applicationContext, R.color.color_eeeeee diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt index 34bb20e..f9b36ce 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt @@ -11,7 +11,7 @@ import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.live.LiveRepository import kr.co.vividnext.sodalive.live.room.LiveRoomType -import kr.co.vividnext.sodalive.live.room.menu.GetMenuResponse +import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody @@ -89,7 +89,7 @@ class LiveRoomCreateViewModel( private var menuId = 0L var menu = "" - val menuList = mutableListOf() + val menuList = mutableListOf() fun setRoomType(roomType: LiveRoomType) { if (_roomTypeLiveData.value!! != roomType) { @@ -346,8 +346,6 @@ class LiveRoomCreateViewModel( menuList.clear() menuList.addAll(data ?: listOf()) selectMenuPreset(SelectedMenu.MENU_1) - - Logger.e("data: $data") } else { if (it.message != null) { _toastLiveData.postValue(it.message) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/GetMenuResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/GetMenuPresetResponse.kt similarity index 51% rename from app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/GetMenuResponse.kt rename to app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/GetMenuPresetResponse.kt index 037afa1..e11fb2d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/GetMenuResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/GetMenuPresetResponse.kt @@ -2,7 +2,8 @@ package kr.co.vividnext.sodalive.live.room.menu import com.google.gson.annotations.SerializedName -data class GetMenuResponse( +data class GetMenuPresetResponse( @SerializedName("id") val id: Long, - @SerializedName("menu") val menu: String + @SerializedName("menu") val menu: String, + @SerializedName("isActive") val isActive: Boolean ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/MenuApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/MenuApi.kt index f17042f..dffa1d5 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/MenuApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/menu/MenuApi.kt @@ -11,5 +11,5 @@ interface MenuApi { fun getAllMenu( @Query("creatorId") creatorId: Long, @Header("Authorization") authHeader: String - ): Single>> + ): Single>> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/EditLiveRoomInfoRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/EditLiveRoomInfoRequest.kt index af67751..e96baf0 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/EditLiveRoomInfoRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/EditLiveRoomInfoRequest.kt @@ -7,5 +7,8 @@ data class EditLiveRoomInfoRequest( @SerializedName("notice") val notice: String?, @SerializedName("numberOfPeople") val numberOfPeople: Int?, @SerializedName("beginDateTimeString") val beginDateTimeString: String?, - @SerializedName("timezone") val timezone: String? + @SerializedName("timezone") val timezone: String?, + @SerializedName("menuPanId") val menuPanId: Long = 0, + @SerializedName("menuPan") val menuPan: String = "", + @SerializedName("isActiveMenuPan") val isActiveMenuPan: Boolean? = null ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt index c7db371..a2e87bf 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt @@ -1,20 +1,29 @@ package kr.co.vividnext.sodalive.live.room.update -import android.app.Activity import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.net.Uri import android.view.LayoutInflater +import android.view.View import android.view.WindowManager +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.lifecycle.MutableLiveData import coil.load import coil.transform.RoundedCornersTransformation import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.databinding.DialogLiveRoomInfoUpdateBinding import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel +import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse class LiveRoomInfoEditDialog( - activity: Activity, + private val activity: AppCompatActivity, layoutInflater: LayoutInflater, onClickImagePicker: () -> Unit ) { @@ -24,6 +33,17 @@ class LiveRoomInfoEditDialog( private var coverImageUrl: String? = null private var coverImageUri: Uri? = null + private var menuId = 0L + private val menuList = mutableListOf() + + private val isActivateMenuLiveData = MutableLiveData(false) + private val selectedMenuLiveData = MutableLiveData() + + private var menu: String = "" + private var isActivateMenu: Boolean? = null + + private lateinit var selectedMenu: LiveRoomCreateViewModel.SelectedMenu + init { val dialogBuilder = AlertDialog.Builder(activity) dialogBuilder.setView(dialogView.root) @@ -35,6 +55,60 @@ class LiveRoomInfoEditDialog( dialogView.ivPhotoPicker.setOnClickListener { onClickImagePicker() } dialogView.ivClose.setOnClickListener { alertDialog.dismiss() } dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() } + + dialogView.ivSwitch.setOnClickListener { + isActivateMenuLiveData.value = !isActivateMenuLiveData.value!! + isActivateMenu = isActivateMenuLiveData.value!! + + if (selectedMenuLiveData.value == null) { + selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_1) + } + } + + dialogView.llSelectMenu1.setOnClickListener { + selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_1) + } + + dialogView.llSelectMenu2.setOnClickListener { + selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_2) + } + + dialogView.llSelectMenu3.setOnClickListener { + selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_3) + } + + selectedMenuLiveData.observe(activity) { + deselectAllMenuPreset() + + when (it) { + LiveRoomCreateViewModel.SelectedMenu.MENU_2 -> selectMenuPresetButton( + dialogView.ivSelectMenu2, + dialogView.llSelectMenu2, + dialogView.tvSelectMenu2 + ) + + LiveRoomCreateViewModel.SelectedMenu.MENU_3 -> selectMenuPresetButton( + dialogView.ivSelectMenu3, + dialogView.llSelectMenu3, + dialogView.tvSelectMenu3 + ) + + else -> selectMenuPresetButton( + dialogView.ivSelectMenu1, + dialogView.llSelectMenu1, + dialogView.tvSelectMenu1 + ) + } + } + isActivateMenuLiveData.observe(activity) { + if (it) { + dialogView.llEditMenu.visibility = View.VISIBLE + dialogView.ivSwitch.setImageResource(R.drawable.btn_toggle_on_big) + } else { + dialogView.llEditMenu.visibility = View.GONE + dialogView.ivSwitch.setImageResource(R.drawable.btn_toggle_off_big) + } + } } fun setRoomInfo( @@ -63,13 +137,47 @@ class LiveRoomInfoEditDialog( } } - fun setConfirmAction(confirmAction: (String, String, Uri?) -> Unit) { + fun setMenuPreset(menuList: List) { + this.menuList.clear() + this.isActivateMenu = null + this.menuList.addAll(menuList) + this.selectedMenuLiveData.value = null + menuList.forEachIndexed { index, menuPreset -> + if (menuPreset.isActive) { + selectedMenu = when (index) { + 1 -> LiveRoomCreateViewModel.SelectedMenu.MENU_2 + 2 -> LiveRoomCreateViewModel.SelectedMenu.MENU_3 + else -> LiveRoomCreateViewModel.SelectedMenu.MENU_1 + } + + isActivateMenuLiveData.value = true + selectMenuPreset(selectedMenu) + } + } + } + + fun setConfirmAction(confirmAction: (String, String, Uri?, Boolean?, Long, String) -> Unit) { dialogView.tvConfirm.setOnClickListener { alertDialog.dismiss() val newTitle = dialogView.etTitle.text.toString() val newContent = dialogView.etContent.text.toString() - confirmAction(newTitle, newContent, coverImageUri) + val menu = dialogView.etMenu.text.toString() + + confirmAction( + newTitle, + newContent, + coverImageUri, + if (isActivateMenu != null) { + isActivateMenu + } else if (this.menu != menu || this.selectedMenu != selectedMenuLiveData.value!!) { + true + } else { + isActivateMenu + }, + menuId, + menu + ) coverImageUri = null coverImageUrl = null } @@ -85,4 +193,103 @@ class LiveRoomInfoEditDialog( alertDialog.window?.attributes = lp } + + private fun deselectAllMenuPreset() { + dialogView.ivSelectMenu1.visibility = View.GONE + dialogView.ivSelectMenu2.visibility = View.GONE + dialogView.ivSelectMenu3.visibility = View.GONE + + dialogView.llSelectMenu1.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b) + dialogView.tvSelectMenu1.setTextColor( + ContextCompat.getColor( + activity, + R.color.color_3bb9f1 + ) + ) + + if (menuList.size > 0) { + dialogView.llSelectMenu2.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b) + dialogView.tvSelectMenu2.setTextColor( + ContextCompat.getColor( + activity, + R.color.color_3bb9f1 + ) + ) + } else { + dialogView.llSelectMenu2.setBackgroundResource(R.drawable.bg_round_corner_6_7_777777) + dialogView.tvSelectMenu2.setTextColor( + ContextCompat.getColor( + activity, + R.color.color_555555 + ) + ) + } + + if (menuList.size > 1) { + dialogView.llSelectMenu3.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b) + dialogView.tvSelectMenu3.setTextColor( + ContextCompat.getColor( + activity, + R.color.color_3bb9f1 + ) + ) + } else { + dialogView.llSelectMenu3.setBackgroundResource(R.drawable.bg_round_corner_6_7_777777) + dialogView.tvSelectMenu3.setTextColor( + ContextCompat.getColor( + activity, + R.color.color_555555 + ) + ) + } + } + + private fun selectMenuPresetButton( + ivSelectMenuPreset: ImageView, + llSelectMenuPreset: LinearLayout, + tvSelectMenuPreset: TextView + ) { + ivSelectMenuPreset.visibility = View.VISIBLE + llSelectMenuPreset.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1) + tvSelectMenuPreset.setTextColor( + ContextCompat.getColor( + activity, + R.color.color_eeeeee + ) + ) + } + + private fun selectMenuPreset(selectedMenuPreset: LiveRoomCreateViewModel.SelectedMenu) { + if ( + menuList.isEmpty() && + ( + selectedMenuPreset == LiveRoomCreateViewModel.SelectedMenu.MENU_2 || + selectedMenuPreset == LiveRoomCreateViewModel.SelectedMenu.MENU_3 + ) + ) { + Toast.makeText(activity, "메뉴 1을 먼저 설정하세요", Toast.LENGTH_SHORT).show() + return + } + + if (menuList.size == 1 && selectedMenuPreset == LiveRoomCreateViewModel.SelectedMenu.MENU_3) { + Toast.makeText(activity, "메뉴 1과 메뉴 2를 먼저 설정하세요", Toast.LENGTH_SHORT).show() + return + } + + if (selectedMenuLiveData.value != selectedMenuPreset) { + selectedMenuLiveData.value = selectedMenuPreset + + if (menuList.size > selectedMenuPreset.ordinal) { + val menuPreset = menuList[selectedMenuPreset.ordinal] + + menu = menuPreset.menu + menuId = menuPreset.id + dialogView.etMenu.setText(menuPreset.menu) + } else { + menu = "" + menuId = 0 + dialogView.etMenu.setText("") + } + } + } } diff --git a/app/src/main/res/layout/dialog_live_room_info_update.xml b/app/src/main/res/layout/dialog_live_room_info_update.xml index 40f8c87..d0db38a 100644 --- a/app/src/main/res/layout/dialog_live_room_info_update.xml +++ b/app/src/main/res/layout/dialog_live_room_info_update.xml @@ -135,6 +135,169 @@ tools:ignore="LabelFor" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +