Skip to content

Commit

Permalink
Merge pull request #539 from Varsha-Kulkarni/pin_notes
Browse files Browse the repository at this point in the history
[Android] Implement feature: Pinning/Unpinning notes
  • Loading branch information
PatilShreyas authored Oct 5, 2022
2 parents 3ebfd72 + 0cc6886 commit 78a1a90
Show file tree
Hide file tree
Showing 25 changed files with 264 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class NoteCardTest : NotyComposableTest() {
fun testNoteCard() = runTest {
var clickCount = 0
setContent {
NoteCard(title = "Lorem Ipsum", note = "Hello World") {
NoteCard(title = "Lorem Ipsum", note = "Hello World", isPinned = false) {
clickCount++
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import dev.shreyaspatil.noty.R

@Composable
fun PinAction(isPinned: Boolean, onClick: () -> Unit) {
val icon = painterResource(id = if (isPinned) R.drawable.ic_pinned else R.drawable.ic_unpinned)
IconButton(onClick = onClick) {
Icon(
painter = icon,
contentDescription = "Pinned Note",
modifier = Modifier
.padding(8.dp)
)
}
}

@Composable
fun DeleteAction(onClick: () -> Unit) {
val icon = painterResource(R.drawable.ic_delete)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,30 @@ package dev.shreyaspatil.noty.composeapp.component.note

import androidx.compose.foundation.clickable
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.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.MaterialTheme.typography
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.shreyaspatil.noty.composeapp.R
import dev.shreyaspatil.noty.composeapp.utils.NotyPreview

@Composable
fun NoteCard(title: String, note: String, onNoteClick: () -> Unit) {
fun NoteCard(title: String, note: String, isPinned: Boolean, onNoteClick: () -> Unit) {
Card(
shape = RoundedCornerShape(4.dp),
backgroundColor = MaterialTheme.colors.surface,
Expand All @@ -52,12 +56,22 @@ fun NoteCard(title: String, note: String, onNoteClick: () -> Unit) {
modifier = Modifier
.padding(16.dp)
) {
Text(
text = title,
style = typography.h5,
color = MaterialTheme.colors.onPrimary,
fontWeight = FontWeight.Bold
)
Row(Modifier.fillMaxWidth()) {
Text(
text = title,
style = typography.h5,
color = MaterialTheme.colors.onPrimary,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
if (isPinned) {
Icon(
painterResource(id = R.drawable.ic_pinned),
contentDescription = "Pinned Note",
modifier = Modifier.padding(8.dp)
)
}
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = note,
Expand All @@ -73,5 +87,10 @@ fun NoteCard(title: String, note: String, onNoteClick: () -> Unit) {
@Preview
@Composable
fun PreviewNoteCard() = NotyPreview {
NoteCard(title = "Lorem Ipsum", note = "Here is note body...", onNoteClick = {})
NoteCard(
title = "Lorem Ipsum",
note = "Here is note body...",
isPinned = true,
onNoteClick = {}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fun NotesList(notes: List<Note>, onClick: (Note) -> Unit) {
NoteCard(
title = note.title,
note = note.note,
isPinned = note.isPinned,
onNoteClick = { onClick(note) }
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import dev.shreyaspatil.capturable.Capturable
import dev.shreyaspatil.capturable.controller.CaptureController
import dev.shreyaspatil.capturable.controller.rememberCaptureController
import dev.shreyaspatil.noty.composeapp.component.action.DeleteAction
import dev.shreyaspatil.noty.composeapp.component.action.PinAction
import dev.shreyaspatil.noty.composeapp.component.action.ShareAction
import dev.shreyaspatil.noty.composeapp.component.action.ShareActionItem
import dev.shreyaspatil.noty.composeapp.component.action.ShareDropdown
Expand Down Expand Up @@ -76,9 +77,11 @@ fun NoteDetailsScreen(
title = state.title ?: "",
note = state.note ?: "",
error = state.error,
isPinned = state.isPinned,
showSaveButton = state.showSave,
onTitleChange = viewModel::setTitle,
onNoteChange = viewModel::setNote,
onPinClick = viewModel::togglePin,
onSaveClick = viewModel::save,
onDeleteClick = { showDeleteNoteConfirmation = true },
onNavigateUp = onNavigateUp,
Expand Down Expand Up @@ -109,9 +112,11 @@ fun NoteDetailContent(
title: String,
note: String,
error: String?,
isPinned: Boolean,
showSaveButton: Boolean,
onTitleChange: (String) -> Unit,
onNoteChange: (String) -> Unit,
onPinClick: () -> Unit,
onSaveClick: () -> Unit,
onNavigateUp: () -> Unit,
onDeleteClick: () -> Unit,
Expand All @@ -131,6 +136,8 @@ fun NoteDetailContent(
onNavigateUp = onNavigateUp,
actions = {
NoteDetailActions(
isPinned = isPinned,
onPinClick = onPinClick,
onDeleteClick = onDeleteClick,
onShareNoteAsTextClick = onShareNoteAsText,
onShareNoteAsImageClick = {
Expand Down Expand Up @@ -166,11 +173,14 @@ fun NoteDetailContent(

@Composable
private fun NoteDetailActions(
isPinned: Boolean,
onPinClick: () -> Unit,
onDeleteClick: () -> Unit,
onShareNoteAsTextClick: () -> Unit,
onShareNoteAsImageClick: () -> Unit
) {
var dropdownExpanded by remember { mutableStateOf(false) }
PinAction(isPinned, onClick = onPinClick)
DeleteAction(onClick = onDeleteClick)
ShareAction(onClick = { dropdownExpanded = true })
ShareDropdown(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class NoteDetailFragment :
*/
private var isNoteLoaded = false

private var pinMenuItem: MenuItem? = null

override val viewModel: NoteDetailViewModel by viewModels {
args.noteId?.let { noteId ->
NoteDetailViewModel.provideFactory(viewModelAssistedFactory, noteId)
Expand Down Expand Up @@ -95,8 +97,6 @@ class NoteDetailFragment :
}

override fun render(state: NoteDetailState) {
showProgressDialog(state.isLoading)

binding.fabSave.isVisible = state.showSave

val title = state.title
Expand All @@ -116,6 +116,15 @@ class NoteDetailFragment :
if (errorMessage != null) {
toast("Error: $errorMessage")
}

updatePinnedIcon(state.isPinned)
}

private fun updatePinnedIcon(isPinned: Boolean) {
pinMenuItem?.run {
val icon = if (isPinned) R.drawable.ic_pinned else R.drawable.ic_unpinned
setIcon(icon)
}
}

private fun shareText() {
Expand Down Expand Up @@ -151,9 +160,15 @@ class NoteDetailFragment :
super.onCreateOptionsMenu(menu, inflater)
}

override fun onPrepareOptionsMenu(menu: Menu) {
pinMenuItem = menu.findItem(R.id.action_pin)
super.onPrepareOptionsMenu(menu)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_delete -> confirmNoteDeletion()
R.id.action_pin -> viewModel.togglePin()
R.id.action_share_text -> shareText()
R.id.action_share_image -> shareImage()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package dev.shreyaspatil.noty.simpleapp.view.notes.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
Expand Down Expand Up @@ -48,6 +49,7 @@ class NotesListAdapter(
with(binding) {
textTitle.text = note.title
textNote.text = note.note
pinnedIcon.isVisible = note.isPinned
root.setOnClickListener { onNoteClick(note) }
}
}
Expand Down
11 changes: 11 additions & 0 deletions noty-android/app/simpleapp/src/main/res/layout/item_note.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@
app:layout_constraintBottom_toBottomOf="parent"
tools:text="@tools:sample/lorem/random" />

<ImageView
android:id="@+id/pinnedIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_pinned"
android:layout_marginTop="@dimen/dimen_8"
android:layout_marginEnd="@dimen/dimen_8"
android:visibility="gone"/>

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>
6 changes: 6 additions & 0 deletions noty-android/app/simpleapp/src/main/res/menu/note_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<item
android:id="@+id/action_pin"
android:icon="@drawable/ic_unpinned"
android:title="@string/menu_pin"
app:showAsAction="ifRoom" />

<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ data class NoteDetailState(
val isLoading: Boolean = false,
val title: String? = null,
val note: String? = null,
val isPinned: Boolean = false,
val showSave: Boolean = false,
val finished: Boolean = false,
val error: String? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ class NoteDetailViewModel @AssistedInject constructor(
if (note != null) {
currentNote = note
setState { state ->
state.copy(isLoading = false, title = note.title, note = note.note)
state.copy(
isLoading = false,
title = note.title,
note = note.note,
isPinned = note.isPinned
)
}
} else {
setState { state -> state.copy(isLoading = false, finished = true) }
Expand Down Expand Up @@ -119,6 +124,25 @@ class NoteDetailViewModel @AssistedInject constructor(
}
}

fun togglePin() {
job?.cancel()
job = viewModelScope.launch {
setState { state -> state.copy(isLoading = true) }

val response = noteRepository.pinNote(noteId, !currentState.isPinned)

setState { state -> state.copy(isLoading = false, isPinned = !currentState.isPinned) }

response.onSuccess { noteId ->
if (!NotyNoteRepository.isTemporaryNote(noteId)) {
scheduleNoteUpdatePin(noteId)
}
}.onFailure { message ->
setState { state -> state.copy(error = message) }
}
}
}

private fun validateNote() {
try {
val oldTitle = currentNote.title
Expand All @@ -144,6 +168,9 @@ class NoteDetailViewModel @AssistedInject constructor(
private fun scheduleNoteDelete(noteId: String) =
notyTaskManager.scheduleTask(NotyTask.delete(noteId))

private fun scheduleNoteUpdatePin(noteId: String) =
notyTaskManager.scheduleTask(NotyTask.pin(noteId))

@AssistedFactory
interface Factory {
fun create(noteId: String): NoteDetailViewModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class NotyTaskWorker @AssistedInject constructor(
NotyTaskAction.CREATE -> addNote(noteId)
NotyTaskAction.UPDATE -> updateNote(noteId)
NotyTaskAction.DELETE -> deleteNote(noteId)
NotyTaskAction.PIN -> pinNote(noteId)
}
}

Expand All @@ -56,6 +57,12 @@ class NotyTaskWorker @AssistedInject constructor(
return if (response is Either.Success) Result.success() else Result.retry()
}

private suspend fun pinNote(noteId: String): Result {
val note = fetchLocalNote(noteId)
val response = remoteNoteRepository.pinNote(noteId, note.isPinned)
return if (response is Either.Success) Result.success() else Result.retry()
}

private suspend fun fetchLocalNote(noteId: String): Note =
localNoteRepository.getNoteById(noteId).first()

Expand Down
6 changes: 6 additions & 0 deletions noty-android/app/src/main/res/drawable/ic_pinned.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="?android:colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white"
android:fillType="evenOdd" android:pathData="M16,9V4l1,0c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H7C6.45,2 6,2.45 6,3v0c0,0.55 0.45,1 1,1l1,0v5c0,1.66 -1.34,3 -3,3h0v2h5.97v7l1,1l1,-1v-7H19v-2h0C17.34,12 16,10.66 16,9z"/>
</vector>
5 changes: 5 additions & 0 deletions noty-android/app/src/main/res/drawable/ic_unpinned.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?android:colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M14,4v5c0,1.12 0.37,2.16 1,3H9c0.65,-0.86 1,-1.9 1,-3V4H14M17,2H7C6.45,2 6,2.45 6,3c0,0.55 0.45,1 1,1c0,0 0,0 0,0l1,0v5c0,1.66 -1.34,3 -3,3v2h5.97v7l1,1l1,-1v-7H19v-2c0,0 0,0 0,0c-1.66,0 -3,-1.34 -3,-3V4l1,0c0,0 0,0 0,0c0.55,0 1,-0.45 1,-1C18,2.45 17.55,2 17,2L17,2z"/>
</vector>
1 change: 1 addition & 0 deletions noty-android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<!-- Action Menu -->
<string name="menu_ui_mode">Light/Dark Mode</string>
<string name="menu_delete">Delete</string>
<string name="menu_pin">Pin</string>
<string name="menu_share">Share</string>
<string name="menu_share_text">Share as Text</string>
<string name="menu_share_image">Share as Image</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ data class Note(
val id: String,
val title: String,
val note: String,
val created: Long
val created: Long,
val isPinned: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ class NotyTask private constructor(val noteId: String, val action: NotyTaskActio
fun create(noteId: String) = NotyTask(noteId, NotyTaskAction.CREATE)
fun update(noteId: String) = NotyTask(noteId, NotyTaskAction.UPDATE)
fun delete(noteId: String) = NotyTask(noteId, NotyTaskAction.DELETE)
fun pin(noteId: String) = NotyTask(noteId, NotyTaskAction.PIN)
}
}

enum class NotyTaskAction {
CREATE, UPDATE, DELETE
CREATE, UPDATE, DELETE, PIN
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ interface NotyNoteRepository {
*/
suspend fun deleteNote(noteId: String): Either<String>

/**
* Pins/unpins a note having ID [noteId] based on [isPinned]
*/
suspend fun pinNote(noteId: String, isPinned: Boolean): Either<String>

/**
* Deletes all notes.
*/
Expand Down
Loading

0 comments on commit 78a1a90

Please sign in to comment.