Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrated Custom Image Selector to Jetpack Compose #5964

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b0bf55b
UI: add material theme for compose
rohit9625 Sep 20, 2024
3f3df7d
add ui components for custom selector
rohit9625 Sep 20, 2024
8558594
add adaptive layout, image loading, and compose navigation dependencies
rohit9625 Oct 3, 2024
74cf035
add logic to fetch images from storage and manage in viewmodel
rohit9625 Oct 3, 2024
9f7ae75
create custom selector main screen with UI events and folder data class
rohit9625 Oct 3, 2024
0ff2880
create image grid screen/pane to display images from any folder
rohit9625 Oct 3, 2024
5da9097
refactor: add new cs screen into custom selector activity
rohit9625 Oct 3, 2024
1a86883
add actions lambda to bottom bar and replace hard-coded strings
rohit9625 Oct 13, 2024
ca30bf1
Add selection count indicator in top bar and refactor
rohit9625 Oct 13, 2024
55c0939
Add drag and tap gestures to select images
rohit9625 Oct 13, 2024
31c012f
refactor holder screen for both folder and image panes
rohit9625 Oct 13, 2024
071bffb
refactor preview for folder item
rohit9625 Oct 13, 2024
a930d8e
add view image screen and enable edge to edge for custom selector
rohit9625 Nov 25, 2024
dcd31f9
update dependencies
rohit9625 Nov 25, 2024
130158f
remove state-changing argument causing unnecessary recompositions
rohit9625 Nov 25, 2024
03713dd
add functionality for unselecting all pictures at once
rohit9625 Nov 26, 2024
178154c
fix overlapping navigation bar adding navigation bar padding
rohit9625 Nov 26, 2024
4ebb945
Merge branch 'main' into jetpack-custom-selector
rohit9625 Nov 27, 2024
c688059
remove onBackPressed override
rohit9625 Nov 27, 2024
c033003
move models package inside domain package
rohit9625 Nov 29, 2024
1d11ab7
fix imports and refactor code
rohit9625 Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ dependencies {

implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
implementation "com.google.android.material:material:1.9.0"
implementation "com.google.android.material:material:1.12.0"
implementation 'com.karumi:dexter:5.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

// Jetpack Compose
def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
def composeBom = platform('androidx.compose:compose-bom:2024.10.00')

implementation "androidx.activity:activity-compose:1.9.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
implementation "androidx.activity:activity-compose:1.9.3"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.6"
implementation (composeBom)
implementation "androidx.compose.runtime:runtime"
implementation "androidx.compose.ui:ui"
Expand All @@ -65,6 +65,13 @@ dependencies {
implementation "androidx.compose.foundation:foundation-layout"
implementation "androidx.compose.material3:material3"
androidTestImplementation(composeBom)
// Adaptive Layout APIs
implementation "androidx.compose.material3.adaptive:adaptive:1.0.0"
implementation "androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0"
implementation "androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0"

implementation "io.coil-kt:coil-compose:2.6.0"
implementation "androidx.navigation:navigation-compose:2.8.3"

implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION"
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
android:label="@string/result" />
<activity
android:name=".customselector.ui.selector.CustomSelectorActivity"
android:windowSoftInputMode="adjustResize"
android:configChanges="screenSize|keyboard|orientation"
android:label="@string/title_activity_custom_selector"
android:parentActivityName=".contributions.MainActivity" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package fr.free.nrw.commons.customselector.data

import android.content.ContentUris
import android.content.Context
import android.provider.MediaStore
import android.text.format.DateFormat
import fr.free.nrw.commons.customselector.domain.model.Image
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.util.Calendar
import java.util.Date
import javax.inject.Inject

class MediaReader @Inject constructor(private val context: Context) {
fun getImages() = flow {
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.MIME_TYPE
)
val cursor = context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection,
null, null, MediaStore.Images.Media.DATE_ADDED + " DESC"
)

cursor?.use {
val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val bucketIdColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)
val bucketNameColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
val dateColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)
val mimeTypeColumn = cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)

while(cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val path = cursor.getString(dataColumn)
val bucketId = cursor.getLong(bucketIdColumn)
val bucketName = cursor.getString(bucketNameColumn)
val date = cursor.getLong(dateColumn)
val mimeType = cursor.getString(mimeTypeColumn)

val validMimeTypes = arrayOf(
"image/jpeg", "image/png", "image/svg+xml", "image/gif",
"image/tiff", "image/webp", "image/x-xcf"
)
// Skip the media items with unsupported MIME types
if(mimeType.lowercase() !in validMimeTypes) continue

// URI to access the image
val uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id
)

val calendar = Calendar.getInstance()
calendar.timeInMillis = date * 1000L
val calendarDate: Date = calendar.time
val dateFormat = DateFormat.getMediumDateFormat(context)
val formattedDate = dateFormat.format(calendarDate)

emit(Image(id, name, uri, path, bucketId, bucketName, date = formattedDate))
}
}
}.flowOn(Dispatchers.IO)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fr.free.nrw.commons.customselector.model
package fr.free.nrw.commons.customselector.domain.model

/**
* sealed class Callback Status.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fr.free.nrw.commons.customselector.model
package fr.free.nrw.commons.customselector.domain.model

/**
* Custom selector data class Folder.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fr.free.nrw.commons.customselector.model
package fr.free.nrw.commons.customselector.domain.model

import android.net.Uri
import android.os.Parcel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fr.free.nrw.commons.customselector.model
package fr.free.nrw.commons.customselector.domain.model

/**
* Custom selector data class Result.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package fr.free.nrw.commons.customselector.helper

import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.domain.model.Folder
import fr.free.nrw.commons.customselector.domain.model.Image

/**
* Image Helper object, includes all the static functions and variables required by custom selector.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package fr.free.nrw.commons.customselector.listeners

import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.domain.model.Image

/**
* Custom Selector Image Loader Listener
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package fr.free.nrw.commons.customselector.listeners

import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.domain.model.Image

/**
* Custom selector Image select listener
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package fr.free.nrw.commons.customselector.listeners

import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.domain.model.Image

/**
* Interface to pass data between fragment and activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.domain.model.Folder
import fr.free.nrw.commons.customselector.domain.model.Image

/**
* Custom selector FolderAdapter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.domain.model.Image
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package fr.free.nrw.commons.customselector.ui.components

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import fr.free.nrw.commons.ui.theme.CommonsTheme

@Composable
fun PrimaryButton(
text: String,
onClick: ()-> Unit,
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(12.dp),
) {
Button(
onClick = onClick,
modifier = modifier,
shape = shape,
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 4.dp)
) {
Text(
text = text,
textAlign = TextAlign.Center
)
}
}

@Composable
fun SecondaryButton(
text: String,
onClick: ()-> Unit,
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(12.dp),
) {
OutlinedButton(
onClick = onClick,
modifier = modifier,
border = BorderStroke(1.dp, color = MaterialTheme.colorScheme.primary),
shape = shape,
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp)
) {
Text(
text = text,
textAlign = TextAlign.Center
)
}
}

@PreviewLightDark
@Composable
private fun PrimaryButtonPreview() {
CommonsTheme {
Surface {
PrimaryButton(
text = "Primary Button",
onClick = { },
modifier = Modifier.padding(16.dp)
)
}
}
}

@PreviewLightDark
@Composable
private fun SecondaryButtonPreview() {
CommonsTheme {
Surface {
SecondaryButton(
text = "Secondary Button",
onClick = { },
modifier = Modifier.padding(16.dp)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package fr.free.nrw.commons.customselector.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import fr.free.nrw.commons.R
import fr.free.nrw.commons.ui.theme.CommonsTheme

@Composable
fun CustomSelectorBottomBar(
onPrimaryAction: ()-> Unit,
onSecondaryAction: ()-> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
SecondaryButton(
text = stringResource(R.string.mark_as_not_for_upload).uppercase(),
onClick = onSecondaryAction,
modifier = Modifier.weight(1f)
)

PrimaryButton(
text = stringResource(R.string.upload).uppercase(),
onClick = onPrimaryAction,
modifier = Modifier
.weight(1f)
.height(IntrinsicSize.Max)
)
}
}

@PreviewLightDark
@Composable
private fun CustomSelectorBottomBarPreview() {
CommonsTheme {
Surface(tonalElevation = 3.dp) {
CustomSelectorBottomBar(
onPrimaryAction = { },
onSecondaryAction = { },
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth()
)
}
}
}
Loading
Loading