浏览代码

added bottom sheet with copy and share actions

sirekanian 3 年之前
父节点
当前提交
1f5599a13e

+ 3 - 0
app/src/main/java/com/sirekanian/acf/D.kt

@@ -15,4 +15,7 @@ object D {
     val cardSelectedElevation = 4.dp
     val cardSelectedElevation = 4.dp
     val cardSpacing = 8.dp
     val cardSpacing = 8.dp
     val progressSize = 2.dp
     val progressSize = 2.dp
+    val dialogTitlePadding = 16.dp
+    val menuIconPadding = 16.dp
+    val menuTextPadding = 8.dp
 }
 }

+ 18 - 14
app/src/main/java/com/sirekanian/acf/MainActivity.kt

@@ -18,10 +18,8 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.Dp
 import androidx.core.view.WindowCompat
 import androidx.core.view.WindowCompat
 import com.sirekanian.acf.ext.app
 import com.sirekanian.acf.ext.app
-import com.sirekanian.acf.ui.MainContent
-import com.sirekanian.acf.ui.MainFab
-import com.sirekanian.acf.ui.MainProgress
-import com.sirekanian.acf.ui.MainToolbar
+import com.sirekanian.acf.ext.isCyrillicResources
+import com.sirekanian.acf.ui.*
 import com.sirekanian.acf.ui.theme.WarmongrTheme
 import com.sirekanian.acf.ui.theme.WarmongrTheme
 
 
 class MainActivity : ComponentActivity() {
 class MainActivity : ComponentActivity() {
@@ -29,7 +27,9 @@ class MainActivity : ComponentActivity() {
         super.onCreate(savedInstanceState)
         super.onCreate(savedInstanceState)
         WindowCompat.setDecorFitsSystemWindows(window, false)
         WindowCompat.setDecorFitsSystemWindows(window, false)
         setContent {
         setContent {
-            val state = remember { MainState() }
+            val coroutineScope = rememberCoroutineScope()
+            val isCyrillic = isCyrillicResources()
+            val state = remember { MainState(coroutineScope, isCyrillic) }
             val presenter = remember { createPresenter(app(), state) }
             val presenter = remember { createPresenter(app(), state) }
             val data by presenter.observeData().collectAsState(listOf())
             val data by presenter.observeData().collectAsState(listOf())
             val hasData by derivedStateOf { data.isNotEmpty() }
             val hasData by derivedStateOf { data.isNotEmpty() }
@@ -43,6 +43,7 @@ class MainActivity : ComponentActivity() {
                         .background(MaterialTheme.colors.background)
                         .background(MaterialTheme.colors.background)
                 ) {
                 ) {
                     MainLayout(
                     MainLayout(
+                        state = state,
                         toolbar = { insets ->
                         toolbar = { insets ->
                             MainToolbar(insets, state.search)
                             MainToolbar(insets, state.search)
                             MainProgress(insets, state.progress)
                             MainProgress(insets, state.progress)
@@ -61,6 +62,7 @@ class MainActivity : ComponentActivity() {
 
 
 @Composable
 @Composable
 fun MainLayout(
 fun MainLayout(
+    state: MainState,
     toolbar: @Composable (PaddingValues) -> Unit,
     toolbar: @Composable (PaddingValues) -> Unit,
     toolbarElevation: Dp,
     toolbarElevation: Dp,
     content: @Composable (PaddingValues) -> Unit,
     content: @Composable (PaddingValues) -> Unit,
@@ -68,15 +70,17 @@ fun MainLayout(
     fab: @Composable () -> Unit,
     fab: @Composable () -> Unit,
     fabVisible: Boolean,
     fabVisible: Boolean,
 ) {
 ) {
-    AnimatedVisibility(contentVisible, enter = fadeIn(), exit = fadeOut()) {
-        content(WindowInsets.systemBars.asPaddingValues())
-    }
-    Surface(Modifier.fillMaxWidth(), elevation = toolbarElevation) {
-        toolbar(WindowInsets.statusBars.asPaddingValues())
-    }
-    AnimatedVisibility(fabVisible, enter = fadeIn(), exit = fadeOut()) {
-        BottomBox(Modifier.padding(D.fabPadding)) {
-            fab()
+    MainBottomSheet(dialogState = state.dialog) {
+        AnimatedVisibility(contentVisible, enter = fadeIn(), exit = fadeOut()) {
+            content(WindowInsets.systemBars.asPaddingValues())
+        }
+        Surface(Modifier.fillMaxWidth(), elevation = toolbarElevation) {
+            toolbar(WindowInsets.statusBars.asPaddingValues())
+        }
+        AnimatedVisibility(fabVisible, enter = fadeIn(), exit = fadeOut()) {
+            BottomBox(Modifier.padding(D.fabPadding)) {
+                fab()
+            }
         }
         }
     }
     }
     if (MaterialTheme.colors.isLight) {
     if (MaterialTheme.colors.isLight) {

+ 7 - 2
app/src/main/java/com/sirekanian/acf/MainPresenter.kt

@@ -4,13 +4,14 @@ import com.sirekanian.acf.data.Repository
 import com.sirekanian.acf.data.Warmonger
 import com.sirekanian.acf.data.Warmonger
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 
 
 fun createPresenter(app: App, state: MainState): MainPresenter =
 fun createPresenter(app: App, state: MainState): MainPresenter =
     MainPresenterImpl(app.repository, state)
     MainPresenterImpl(app.repository, state)
 
 
 interface MainPresenter {
 interface MainPresenter {
-    fun observeData(): Flow<List<Warmonger>>
+    fun observeData(): Flow<List<WarmongerModel>>
     suspend fun updateData()
     suspend fun updateData()
 }
 }
 
 
@@ -19,11 +20,15 @@ class MainPresenterImpl(
     private val state: MainState,
     private val state: MainState,
 ) : MainPresenter {
 ) : MainPresenter {
 
 
-    override fun observeData(): Flow<List<Warmonger>> =
+    override fun observeData(): Flow<List<WarmongerModel>> =
         if (state.search.isOpened) {
         if (state.search.isOpened) {
             repository.observeByQuery(state.search.query.text)
             repository.observeByQuery(state.search.query.text)
         } else {
         } else {
             repository.observeAll()
             repository.observeAll()
+        }.map { warmongers ->
+            warmongers.map { warmonger ->
+                Warmonger.toModel(warmonger, state.isCyrillic)
+            }
         }
         }
 
 
     override suspend fun updateData() =
     override suspend fun updateData() =

+ 31 - 2
app/src/main/java/com/sirekanian/acf/MainState.kt

@@ -1,14 +1,19 @@
 package com.sirekanian.acf
 package com.sirekanian.acf
 
 
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue.Hidden
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 
-class MainState {
+class MainState(coroutineScope: CoroutineScope, val isCyrillic: Boolean) {
     val list = ListState()
     val list = ListState()
     val search = SearchState()
     val search = SearchState()
     val progress = ProgressState()
     val progress = ProgressState()
@@ -19,6 +24,7 @@ class MainState {
             0.dp
             0.dp
         }
         }
     }
     }
+    val dialog = DialogState(coroutineScope)
 }
 }
 
 
 class ListState {
 class ListState {
@@ -69,4 +75,27 @@ class ProgressState {
         return (current.toFloat() / total).coerceIn(0f, 1f)
         return (current.toFloat() / total).coerceIn(0f, 1f)
     }
     }
 
 
-}
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+class DialogState(private val coroutineScope: CoroutineScope) {
+
+    val bottomSheetState = ModalBottomSheetState(Hidden)
+    var title by mutableStateOf("")
+    var content by mutableStateOf("")
+
+    fun show(model: WarmongerModel) {
+        title = model.title
+        content = model.content
+        coroutineScope.launch {
+            bottomSheetState.show()
+        }
+    }
+
+    fun hide() {
+        coroutineScope.launch {
+            bottomSheetState.hide()
+        }
+    }
+
+}

+ 5 - 0
app/src/main/java/com/sirekanian/acf/WarmongerModel.kt

@@ -0,0 +1,5 @@
+package com.sirekanian.acf
+
+class WarmongerModel(val title: String, val description: String) {
+    val content get() = "$title\n\n$description"
+}

+ 11 - 4
app/src/main/java/com/sirekanian/acf/data/Warmonger.kt

@@ -1,5 +1,6 @@
 package com.sirekanian.acf.data
 package com.sirekanian.acf.data
 
 
+import com.sirekanian.acf.WarmongerModel
 import com.sirekanian.acf.data.local.WarmongerEntity
 import com.sirekanian.acf.data.local.WarmongerEntity
 import com.sirekanian.acf.data.remote.WarmongerDto
 import com.sirekanian.acf.data.remote.WarmongerDto
 
 
@@ -25,11 +26,17 @@ class Warmonger(
                 notes = entity.notes,
                 notes = entity.notes,
             )
             )
 
 
-        fun toEntity(model: Warmonger): WarmongerEntity =
+        fun toEntity(warmonger: Warmonger): WarmongerEntity =
             WarmongerEntity(
             WarmongerEntity(
-                cyrillicName = model.cyrillicName,
-                name = model.name,
-                notes = model.notes,
+                cyrillicName = warmonger.cyrillicName,
+                name = warmonger.name,
+                notes = warmonger.notes,
+            )
+
+        fun toModel(warmonger: Warmonger, isCyrillic: Boolean): WarmongerModel =
+            WarmongerModel(
+                title = if (isCyrillic) warmonger.cyrillicName else warmonger.name,
+                description = warmonger.notes,
             )
             )
 
 
     }
     }

+ 8 - 0
app/src/main/java/com/sirekanian/acf/ext/Intent.kt

@@ -0,0 +1,8 @@
+package com.sirekanian.acf.ext
+
+import android.content.Intent
+
+fun createShareIntent(text: String): Intent =
+    Intent(Intent.ACTION_SEND)
+        .putExtra(Intent.EXTRA_TEXT, text)
+        .setType("text/plain")

+ 83 - 0
app/src/main/java/com/sirekanian/acf/ui/MainBottomSheet.kt

@@ -0,0 +1,83 @@
+package com.sirekanian.acf.ui
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+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.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import com.sirekanian.acf.D
+import com.sirekanian.acf.DialogState
+import com.sirekanian.acf.R
+import com.sirekanian.acf.ext.createShareIntent
+import com.sirekanian.acf.ui.icons.IconCopy
+import com.sirekanian.acf.ui.icons.IconShare
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun MainBottomSheet(dialogState: DialogState, content: @Composable () -> Unit) {
+    val clipboardManager = LocalClipboardManager.current
+    val context = LocalContext.current
+    ModalBottomSheetLayout(
+        sheetState = dialogState.bottomSheetState,
+        sheetContent = {
+            Column(
+                modifier = Modifier.navigationBarsPadding(),
+            ) {
+                Text(
+                    text = dialogState.title,
+                    modifier = Modifier
+                        .padding(D.dialogTitlePadding)
+                        .alpha(ContentAlpha.medium),
+                    style = MaterialTheme.typography.subtitle1
+                )
+                BottomSheetItem(
+                    icon = IconCopy,
+                    text = stringResource(R.string.app_copy_menu),
+                    onClick = {
+                        clipboardManager.setText(AnnotatedString(dialogState.content))
+                        dialogState.hide()
+                    }
+                )
+                BottomSheetItem(
+                    icon = IconShare,
+                    text = stringResource(R.string.app_share_menu),
+                    onClick = {
+                        context.startActivity(createShareIntent(dialogState.content))
+                    }
+                )
+            }
+        },
+        content = content,
+    )
+}
+
+@Composable
+private fun BottomSheetItem(icon: ImageVector, text: String, onClick: () -> Unit) {
+    Row(
+        modifier = Modifier
+            .clickable(onClick = onClick)
+            .fillMaxWidth(),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        Icon(
+            imageVector = icon,
+            modifier = Modifier
+                .padding(D.menuIconPadding)
+                .alpha(ContentAlpha.medium),
+            contentDescription = text,
+        )
+        Text(
+            text = text,
+            modifier = Modifier
+                .padding(horizontal = D.menuTextPadding),
+            style = MaterialTheme.typography.body1,
+        )
+    }
+}

+ 3 - 3
app/src/main/java/com/sirekanian/acf/ui/MainContent.kt

@@ -11,12 +11,12 @@ import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import com.sirekanian.acf.D
 import com.sirekanian.acf.D
 import com.sirekanian.acf.MainState
 import com.sirekanian.acf.MainState
-import com.sirekanian.acf.data.Warmonger
+import com.sirekanian.acf.WarmongerModel
 import com.sirekanian.acf.ext.plus
 import com.sirekanian.acf.ext.plus
 import com.sirekanian.acf.ext.pointerInputOnDown
 import com.sirekanian.acf.ext.pointerInputOnDown
 
 
 @Composable
 @Composable
-fun MainContent(insets: PaddingValues, state: MainState, data: List<Warmonger>) {
+fun MainContent(insets: PaddingValues, state: MainState, data: List<WarmongerModel>) {
     val bottomPadding = if (state.search.isOpened) 0.dp else D.fabSize + D.fabPadding
     val bottomPadding = if (state.search.isOpened) 0.dp else D.fabSize + D.fabPadding
     val paddings = PaddingValues(top = D.toolbarSize, bottom = bottomPadding)
     val paddings = PaddingValues(top = D.toolbarSize, bottom = bottomPadding)
     val focusManager = LocalFocusManager.current
     val focusManager = LocalFocusManager.current
@@ -29,7 +29,7 @@ fun MainContent(insets: PaddingValues, state: MainState, data: List<Warmonger>)
         verticalArrangement = Arrangement.spacedBy(D.cardSpacing)
         verticalArrangement = Arrangement.spacedBy(D.cardSpacing)
     ) {
     ) {
         items(data) { item ->
         items(data) { item ->
-            WarmongerCard(item)
+            WarmongerCard(state.dialog, item)
         }
         }
     }
     }
 }
 }

+ 21 - 9
app/src/main/java/com/sirekanian/acf/ui/WarmongerCard.kt

@@ -2,7 +2,8 @@ package com.sirekanian.acf.ui
 
 
 import androidx.compose.animation.animateContentSize
 import androidx.compose.animation.animateContentSize
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.ContentAlpha
 import androidx.compose.material.ContentAlpha
@@ -15,11 +16,11 @@ import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import com.sirekanian.acf.D
 import com.sirekanian.acf.D
-import com.sirekanian.acf.data.Warmonger
-import com.sirekanian.acf.ext.isCyrillicResources
+import com.sirekanian.acf.DialogState
+import com.sirekanian.acf.WarmongerModel
 
 
 @Composable
 @Composable
-fun WarmongerCard(warmonger: Warmonger) {
+fun WarmongerCard(dialogState: DialogState, warmonger: WarmongerModel) {
     var isExpanded by remember { mutableStateOf(false) }
     var isExpanded by remember { mutableStateOf(false) }
     val surfaceCornerSize by animateDpAsState(
     val surfaceCornerSize by animateDpAsState(
         if (isExpanded) {
         if (isExpanded) {
@@ -40,26 +41,37 @@ fun WarmongerCard(warmonger: Warmonger) {
         shape = RoundedCornerShape(surfaceCornerSize),
         shape = RoundedCornerShape(surfaceCornerSize),
         elevation = surfaceElevation
         elevation = surfaceElevation
     ) {
     ) {
-        WarmongerCardContent(warmonger, isExpanded) { isExpanded = !isExpanded }
+        WarmongerCardContent(
+            warmonger = warmonger,
+            isExpanded = isExpanded,
+            onClick = { isExpanded = !isExpanded },
+            onLongClick = { dialogState.show(warmonger) },
+        )
     }
     }
 }
 }
 
 
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
 @Composable
-private fun WarmongerCardContent(warmonger: Warmonger, isExpanded: Boolean, onClick: () -> Unit) {
+private fun WarmongerCardContent(
+    warmonger: WarmongerModel,
+    isExpanded: Boolean,
+    onClick: () -> Unit,
+    onLongClick: () -> Unit,
+) {
     Column(
     Column(
         modifier = Modifier
         modifier = Modifier
-            .clickable(onClick = onClick)
+            .combinedClickable(onClick = onClick, onLongClick = onLongClick)
             .padding(24.dp)
             .padding(24.dp)
             .padding(PaddingValues())
             .padding(PaddingValues())
             .animateContentSize()
             .animateContentSize()
     ) {
     ) {
         Text(
         Text(
-            text = if (isCyrillicResources()) warmonger.cyrillicName else warmonger.name,
+            text = warmonger.title,
             style = MaterialTheme.typography.h6
             style = MaterialTheme.typography.h6
         )
         )
         Spacer(Modifier.size(12.dp))
         Spacer(Modifier.size(12.dp))
         Text(
         Text(
-            text = warmonger.notes,
+            text = warmonger.description,
             modifier = Modifier.alpha(ContentAlpha.medium),
             modifier = Modifier.alpha(ContentAlpha.medium),
             style = MaterialTheme.typography.body1,
             style = MaterialTheme.typography.body1,
             maxLines = if (isExpanded) Int.MAX_VALUE else 2,
             maxLines = if (isExpanded) Int.MAX_VALUE else 2,

+ 2 - 0
app/src/main/res/values-be/strings.xml

@@ -3,5 +3,7 @@
     <string name="app_name_part_2">войны</string>
     <string name="app_name_part_2">войны</string>
     <string name="app_name_part_3" />
     <string name="app_name_part_3" />
     <string name="app_search_hint">Поиск…</string>
     <string name="app_search_hint">Поиск…</string>
+    <string name="app_copy_menu">Скопировать</string>
+    <string name="app_share_menu">Поделиться</string>
     <bool name="app_cyrillic">true</bool>
     <bool name="app_cyrillic">true</bool>
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values-ru/strings.xml

@@ -3,5 +3,7 @@
     <string name="app_name_part_2">войны</string>
     <string name="app_name_part_2">войны</string>
     <string name="app_name_part_3" />
     <string name="app_name_part_3" />
     <string name="app_search_hint">Поиск…</string>
     <string name="app_search_hint">Поиск…</string>
+    <string name="app_copy_menu">Скопировать</string>
+    <string name="app_share_menu">Поделиться</string>
     <bool name="app_cyrillic">true</bool>
     <bool name="app_cyrillic">true</bool>
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values-uk/strings.xml

@@ -3,5 +3,7 @@
     <string name="app_name_part_2">войны</string>
     <string name="app_name_part_2">войны</string>
     <string name="app_name_part_3" />
     <string name="app_name_part_3" />
     <string name="app_search_hint">Поиск…</string>
     <string name="app_search_hint">Поиск…</string>
+    <string name="app_copy_menu">Скопировать</string>
+    <string name="app_share_menu">Поделиться</string>
     <bool name="app_cyrillic">true</bool>
     <bool name="app_cyrillic">true</bool>
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -4,5 +4,7 @@
     <string name="app_name_part_2">War</string>
     <string name="app_name_part_2">War</string>
     <string name="app_name_part_3">mongers</string>
     <string name="app_name_part_3">mongers</string>
     <string name="app_search_hint">Search…</string>
     <string name="app_search_hint">Search…</string>
+    <string name="app_copy_menu">Copy</string>
+    <string name="app_share_menu">Share</string>
     <bool name="app_cyrillic">false</bool>
     <bool name="app_cyrillic">false</bool>
 </resources>
 </resources>