WIP
This commit is contained in:
parent
031722920a
commit
77d1cc13b4
@ -58,6 +58,7 @@ dependencies {
|
||||
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.1'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||
@ -156,4 +157,7 @@ dependencies {
|
||||
implementation 'androidx.compose.runtime:runtime-livedata'
|
||||
|
||||
implementation "io.coil-kt.coil3:coil-compose:3.0.1"
|
||||
implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.1")
|
||||
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.36.0")
|
||||
}
|
||||
@ -5,6 +5,8 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<application
|
||||
android:name=".MyApplication"
|
||||
android:allowBackup="false"
|
||||
@ -17,6 +19,7 @@
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
|
||||
<!-- Activities -->
|
||||
<activity
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||
android:theme="@style/Theme.Isolaatti.OSS" />
|
||||
@ -48,10 +51,12 @@
|
||||
<activity android:name=".about.AboutActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".images.picture_viewer.ui.PictureViewerActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".sign_up.ui.SignUpActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".images.image_maker.ui.ImageMakerActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".profile.ui.EditProfileActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
<activity android:name=".posting.posts.ui.PostInfoActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".hashtags.ui.HashtagsPostsActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
|
||||
<!-- End activities-->
|
||||
|
||||
<provider
|
||||
android:authorities="com.isolaatti.provider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
@ -60,6 +65,8 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths"/>
|
||||
</provider>
|
||||
|
||||
<!-- Services -->
|
||||
<service android:name=".push_notifications.FcmService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
@ -67,6 +74,13 @@
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
<service android:name=".audio.player.MediaService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
package com.isolaatti.audio.common.components
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.utils.clockFormat
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@ExperimentalMaterial3Api
|
||||
@Composable
|
||||
fun AudioPlayer(
|
||||
modifier: Modifier,
|
||||
positionMs: Long,
|
||||
isPlaying: Boolean,
|
||||
isLoading: Boolean,
|
||||
durationMs: Long,
|
||||
onPlay: () -> Unit = {},
|
||||
onPause: () -> Unit = {}
|
||||
) {
|
||||
val sliderValue = positionMs.coerceAtLeast(1).toFloat() / durationMs.coerceAtLeast(1).toFloat()
|
||||
|
||||
|
||||
|
||||
Card(modifier = modifier) {
|
||||
|
||||
Row(modifier = Modifier.padding(16.dp)) {
|
||||
IconButton(onClick = {if(isPlaying) onPause() else onPlay()}) {
|
||||
if(isPlaying) {
|
||||
Icon(painterResource(R.drawable.baseline_pause_24), null)
|
||||
} else {
|
||||
Icon(Icons.Default.PlayArrow, null)
|
||||
}
|
||||
}
|
||||
if(isLoading) {
|
||||
LinearProgressIndicator(modifier = Modifier.padding(horizontal = 4.dp).weight(1f))
|
||||
} else {
|
||||
Slider(value = sliderValue, onValueChange = {}, modifier = Modifier.padding(horizontal = 4.dp).weight(1f))
|
||||
}
|
||||
|
||||
Text("${positionMs.milliseconds.clockFormat()}/${durationMs.milliseconds.clockFormat()}")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,34 +1,185 @@
|
||||
package com.isolaatti.audio.common.components
|
||||
|
||||
import android.media.MediaRecorder
|
||||
import android.net.Uri
|
||||
import android.Manifest
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.utils.clockFormat
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AudioRecorder(
|
||||
onAudioRecorded: (Uri) -> Unit,
|
||||
dismissible: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onDismiss: () -> Unit = {},
|
||||
onPlay: () -> Unit = {},
|
||||
onPause: () -> Unit = {},
|
||||
onStopRecording: () -> Unit,
|
||||
onPauseRecording: () -> Unit,
|
||||
onStartRecording: (fromPaused: Boolean) -> Unit,
|
||||
isRecording: Boolean,
|
||||
recordIsStopped: Boolean,
|
||||
recordIsPaused: Boolean,
|
||||
isPlaying: Boolean,
|
||||
isLoading: Boolean,
|
||||
durationSeconds: Long,
|
||||
position: Long,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
var context = LocalContext.current
|
||||
var mediaRecorder: MediaRecorder?
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
mediaRecorder = MediaRecorder()
|
||||
|
||||
onDispose {
|
||||
mediaRecorder?.release()
|
||||
|
||||
val audioRecordPermissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO)
|
||||
|
||||
var duration by remember { mutableLongStateOf(0L) }
|
||||
|
||||
|
||||
LaunchedEffect(isPlaying) {
|
||||
coroutineScope {
|
||||
while(isPlaying) {
|
||||
duration += 500
|
||||
delay(500.milliseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AudioRecorderPreview() {
|
||||
Card(modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
FilledTonalIconButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.zIndex(2f)
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Close, contentDescription = null)
|
||||
|
||||
}
|
||||
Text(stringResource(R.string.record_new_audio))
|
||||
}
|
||||
|
||||
when {
|
||||
audioRecordPermissionState.status.isGranted -> {
|
||||
when {
|
||||
!isRecording -> {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onStartRecording(false)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.baseline_circle_24),
|
||||
null,
|
||||
tint = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isRecording -> {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Text(duration.milliseconds.clockFormat(), modifier = Modifier.padding(horizontal = 4.dp))
|
||||
Spacer(Modifier.weight(1f))
|
||||
IconButton(onClick = {
|
||||
if(recordIsPaused) {
|
||||
onPauseRecording()
|
||||
} else {
|
||||
onStartRecording(true)
|
||||
}
|
||||
|
||||
}) {
|
||||
if(recordIsPaused) {
|
||||
Icon(painterResource(R.drawable.baseline_circle_24), null, tint = Color.Red)
|
||||
} else {
|
||||
Icon(painterResource(R.drawable.baseline_pause_24), null)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
onStopRecording()
|
||||
}) {
|
||||
Icon(painterResource(R.drawable.baseline_stop_24), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recordIsStopped -> {
|
||||
AudioPlayer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
positionMs = position,
|
||||
isLoading = isLoading,
|
||||
isPlaying = isPlaying,
|
||||
durationMs = durationSeconds,
|
||||
onPlay = onPlay,
|
||||
onPause = onPause
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioRecordPermissionState.status.shouldShowRationale -> {
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) {
|
||||
Text(stringResource(R.string.record_audio_permission_required_rationale))
|
||||
Button(
|
||||
onClick = {
|
||||
audioRecordPermissionState.launchPermissionRequest()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.request_permission))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(4.dp), horizontalAlignment = Alignment.Start) {
|
||||
Text(stringResource(R.string.record_audio_permission_required))
|
||||
Button(
|
||||
onClick = {
|
||||
audioRecordPermissionState.launchPermissionRequest()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.request_permission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
app/src/main/java/com/isolaatti/audio/player/MediaService.kt
Normal file
40
app/src/main/java/com/isolaatti/audio/player/MediaService.kt
Normal file
@ -0,0 +1,40 @@
|
||||
package com.isolaatti.audio.player
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
|
||||
class MediaService : MediaSessionService() {
|
||||
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val player = ExoPlayer.Builder(this).build()
|
||||
mediaSession = MediaSession.Builder(this, player).build()
|
||||
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val player = mediaSession?.player
|
||||
if(player?.playWhenReady == true) {
|
||||
player.pause()
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
||||
return mediaSession
|
||||
}
|
||||
}
|
||||
@ -7,10 +7,9 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.isolaatti.images.common.data.dao.ImagesDraftsDao
|
||||
import com.isolaatti.images.common.data.entity.ImageDraftEntity
|
||||
import com.isolaatti.images.common.data.remote.DeleteImagesDto
|
||||
import com.isolaatti.images.common.data.remote.ImagesApi
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.images.common.domain.repository.ImagesRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -28,13 +27,13 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
|
||||
companion object {
|
||||
const val TAG = "ImagesRepositoryImpl"
|
||||
}
|
||||
override fun getImagesOfUser(userId: Int, lastId: String?): Flow<Resource<List<Image>>> = flow {
|
||||
override fun getImagesOfUser(userId: Int, lastId: String?): Flow<Resource<List<RemoteImage>>> = flow {
|
||||
emit(Resource.Loading())
|
||||
try {
|
||||
val response = imagesApi.getImagesOfUser(userId, lastId).awaitResponse()
|
||||
if(response.isSuccessful) {
|
||||
val imagesDto = response.body()
|
||||
val images = imagesDto?.data?.map { Image(it.id) }
|
||||
val images = imagesDto?.data?.map { RemoteImage(it.id) }
|
||||
|
||||
emit(Resource.Success(images))
|
||||
|
||||
@ -46,7 +45,7 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteImages(images: List<Image>): Flow<Resource<Boolean>> = flow {
|
||||
override fun deleteImages(images: List<RemoteImage>): Flow<Resource<Boolean>> = flow {
|
||||
emit(Resource.Loading())
|
||||
val dto = DeleteImagesDto(images.map { it.id })
|
||||
try {
|
||||
@ -62,7 +61,7 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
|
||||
}
|
||||
}
|
||||
|
||||
override fun uploadImage(imageUri: Uri, postId: Long, squadId: String?): Flow<Resource<Image>> = flow {
|
||||
override fun uploadImage(imageUri: Uri, postId: Long, squadId: String?): Flow<Resource<RemoteImage>> = flow {
|
||||
emit(Resource.Loading())
|
||||
var imageInputStream: InputStream? = null
|
||||
try {
|
||||
@ -93,7 +92,7 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
|
||||
emit(Resource.Error(Resource.Error.ErrorType.ServerError))
|
||||
return@flow
|
||||
}
|
||||
val image = Image(imageDto.id)
|
||||
val image = RemoteImage(imageDto.id)
|
||||
emit(Resource.Success(image))
|
||||
} else {
|
||||
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
|
||||
|
||||
@ -1,56 +1,6 @@
|
||||
package com.isolaatti.images.common.domain.entity
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.isolaatti.common.Deletable
|
||||
import com.isolaatti.images.common.data.remote.ImageDto
|
||||
import com.isolaatti.markdown.Generators
|
||||
import com.isolaatti.utils.UrlGen
|
||||
import java.io.Serializable
|
||||
|
||||
data class Image(
|
||||
val id: String
|
||||
): Deletable(), Parcelable {
|
||||
val imageUrl: String get() = UrlGen.imageUrl(id)
|
||||
val smallImageUrl : String get() = UrlGen.imageUrl(id, UrlGen.IMAGE_MODE_SMALL)
|
||||
val reducedImageUrl: String get() = UrlGen.imageUrl(id, UrlGen.IMAGE_MODE_REDUCED)
|
||||
|
||||
val markdown: String get() = Generators.generateImage(imageUrl)
|
||||
|
||||
constructor(parcel: Parcel) : this(parcel.readString()!!)
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Image
|
||||
|
||||
if (id != other.id) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(id)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Image> {
|
||||
override fun createFromParcel(parcel: Parcel): Image {
|
||||
return Image(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Image?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Marker interface to identify images
|
||||
*/
|
||||
interface Image
|
||||
@ -0,0 +1,11 @@
|
||||
package com.isolaatti.images.common.domain.entity
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
/**
|
||||
* An image that is stored in disk. This cannot be deleted by app,
|
||||
* but only is to be uploaded to the server
|
||||
*/
|
||||
data class LocalImage(
|
||||
val uri: Uri
|
||||
) : Image
|
||||
@ -0,0 +1,62 @@
|
||||
package com.isolaatti.images.common.domain.entity
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.isolaatti.common.Deletable
|
||||
import com.isolaatti.markdown.Generators
|
||||
import com.isolaatti.utils.UrlGen
|
||||
|
||||
/**
|
||||
* Represents an image that can be accessed using a remote uri.
|
||||
*
|
||||
* @property imageUrl The full resolution image url
|
||||
* @property smallImageUrl A thumbnail url
|
||||
* @property reducedImageUrl A squared reduced version of the image url
|
||||
* @property markdown Markdown to insert this image in full resolution
|
||||
*/
|
||||
data class RemoteImage(
|
||||
val id: String
|
||||
): Deletable(), Parcelable, Image {
|
||||
val imageUrl: String get() = UrlGen.imageUrl(id)
|
||||
val smallImageUrl : String get() = UrlGen.imageUrl(id, UrlGen.IMAGE_MODE_SMALL)
|
||||
val reducedImageUrl: String get() = UrlGen.imageUrl(id, UrlGen.IMAGE_MODE_REDUCED)
|
||||
|
||||
val markdown: String get() = Generators.generateImage(imageUrl)
|
||||
|
||||
constructor(parcel: Parcel) : this(parcel.readString()!!)
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as RemoteImage
|
||||
|
||||
if (id != other.id) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(id)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<RemoteImage> {
|
||||
override fun createFromParcel(parcel: Parcel): RemoteImage {
|
||||
return RemoteImage(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<RemoteImage?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,12 @@
|
||||
package com.isolaatti.images.common.domain.repository
|
||||
|
||||
import android.net.Uri
|
||||
import com.isolaatti.images.common.data.entity.ImageDraftEntity
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ImagesRepository {
|
||||
fun getImagesOfUser(userId: Int, lastId: String? = null): Flow<Resource<List<Image>>>
|
||||
fun deleteImages(images: List<Image>): Flow<Resource<Boolean>>
|
||||
fun uploadImage(imageUri: Uri, postId: Long, squadId: String?): Flow<Resource<Image>>
|
||||
fun getImagesOfUser(userId: Int, lastId: String? = null): Flow<Resource<List<RemoteImage>>>
|
||||
fun deleteImages(images: List<RemoteImage>): Flow<Resource<Boolean>>
|
||||
fun uploadImage(imageUri: Uri, postId: Long, squadId: String?): Flow<Resource<RemoteImage>>
|
||||
}
|
||||
@ -2,10 +2,10 @@ package com.isolaatti.images.picture_viewer.presentation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.images.picture_viewer.ui.PictureViewerImageWrapperFragment
|
||||
|
||||
class PictureViewerViewPagerAdapter(fragment: Fragment, private val images: Array<Image>) : FragmentStateAdapter(fragment) {
|
||||
class PictureViewerViewPagerAdapter(fragment: Fragment, private val images: Array<RemoteImage>) : FragmentStateAdapter(fragment) {
|
||||
override fun getItemCount(): Int {
|
||||
return images.size
|
||||
}
|
||||
|
||||
@ -5,8 +5,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.isolaatti.databinding.ActivityPictureViewerBinding
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.picture_viewer.presentation.PictureViewerViewPagerAdapter
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
|
||||
class PictureViewerActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityPictureViewerBinding
|
||||
@ -25,7 +24,7 @@ class PictureViewerActivity : AppCompatActivity() {
|
||||
const val EXTRA_IMAGES = "images"
|
||||
const val EXTRA_IMAGE_POSITiON = "position"
|
||||
|
||||
fun startActivityWithImages(context: Context, images: Array<Image>, position: Int = 0) {
|
||||
fun startActivityWithImages(context: Context, images: Array<RemoteImage>, position: Int = 0) {
|
||||
if(images.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -8,14 +8,14 @@ import androidx.fragment.app.Fragment
|
||||
import coil.load
|
||||
import com.isolaatti.common.CoilImageLoader.imageLoader
|
||||
import com.isolaatti.databinding.FragmentTouchImageViewWrapperBinding
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.ortiz.touchview.OnTouchImageViewListener
|
||||
|
||||
|
||||
class PictureViewerImageWrapperFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentTouchImageViewWrapperBinding
|
||||
private var image: Image? = null
|
||||
private var image: RemoteImage? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@ -24,7 +24,7 @@ class PictureViewerImageWrapperFragment : Fragment() {
|
||||
): View {
|
||||
binding = FragmentTouchImageViewWrapperBinding.inflate(inflater)
|
||||
|
||||
image = arguments?.getSerializable(ARGUMENT_IMAGE) as Image
|
||||
image = arguments?.getSerializable(ARGUMENT_IMAGE) as RemoteImage
|
||||
|
||||
return binding.root
|
||||
}
|
||||
@ -50,7 +50,7 @@ class PictureViewerImageWrapperFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
const val ARGUMENT_IMAGE = "image"
|
||||
fun getInstance(image: Image): PictureViewerImageWrapperFragment {
|
||||
fun getInstance(image: RemoteImage): PictureViewerImageWrapperFragment {
|
||||
val fragment = PictureViewerImageWrapperFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(ARGUMENT_IMAGE, image)
|
||||
|
||||
@ -7,14 +7,14 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.isolaatti.databinding.FragmentMainPictureViewerBinding
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.images.picture_viewer.presentation.PictureViewerViewPagerAdapter
|
||||
|
||||
class PictureViewerMainFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentMainPictureViewerBinding
|
||||
|
||||
lateinit var images: Array<Image>
|
||||
lateinit var images: Array<RemoteImage>
|
||||
|
||||
private val onPageChangeCallback = object: ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
@ -36,7 +36,7 @@ class PictureViewerMainFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
images = requireActivity().intent.extras?.getSerializable(PictureViewerActivity.EXTRA_IMAGES) as Array<Image>
|
||||
images = requireActivity().intent.extras?.getSerializable(PictureViewerActivity.EXTRA_IMAGES) as Array<RemoteImage>
|
||||
val position = requireActivity().intent.extras?.getInt(PictureViewerActivity.EXTRA_IMAGE_POSITiON) ?: 0
|
||||
val adapter = PictureViewerViewPagerAdapter(this, images)
|
||||
binding.viewpager.adapter = adapter
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.isolaatti.posting.posts.components
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@ -33,11 +34,14 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import coil3.compose.AsyncImage
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.LocalImage
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
|
||||
@Composable
|
||||
fun PostAttachments(
|
||||
modifier: Modifier = Modifier,
|
||||
images: List<Uri>,
|
||||
images: List<Image>,
|
||||
deletable: Boolean,
|
||||
imageAddable: Boolean,
|
||||
onImageClick: (index: Int) -> Unit,
|
||||
@ -103,14 +107,27 @@ fun PostAttachments(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
when(val image = images[index]) {
|
||||
is LocalImage -> {
|
||||
AsyncImage(
|
||||
model = images[index],
|
||||
model = image.uri,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize().zIndex(1f),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
is RemoteImage -> {
|
||||
if(!image.delete) {
|
||||
AsyncImage(
|
||||
model = image.reducedImageUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize().zIndex(1f),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.isolaatti.images.common.data.remote.ImageDto
|
||||
import com.isolaatti.images.common.data.remote.ImagesApi
|
||||
import com.isolaatti.images.common.domain.entity.LocalImage
|
||||
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
||||
import com.isolaatti.posting.posts.data.remote.DeletePostDto
|
||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
||||
@ -81,7 +82,7 @@ class PostsRepositoryImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun makePost(createPostDto: CreatePostDto, images: List<Uri>): Flow<Resource<PostingSteps>> = flow {
|
||||
override fun makePost(createPostDto: CreatePostDto, images: List<LocalImage>): Flow<Resource<PostingSteps>> = flow {
|
||||
emit(Resource.Loading())
|
||||
try {
|
||||
val hasImages = images.isNotEmpty()
|
||||
@ -103,10 +104,10 @@ class PostsRepositoryImpl @Inject constructor(
|
||||
return@flow
|
||||
}
|
||||
emit(Resource.Success(PostingSteps.UploadingPhotos))
|
||||
images.forEach { imageUri ->
|
||||
images.forEach { localImage ->
|
||||
var imageInputStream: InputStream? = null
|
||||
try {
|
||||
imageInputStream = contentResolver.openInputStream(imageUri)
|
||||
imageInputStream = contentResolver.openInputStream(localImage.uri)
|
||||
val bitmap = BitmapFactory.decodeStream(imageInputStream)
|
||||
|
||||
if(bitmap == null) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.isolaatti.posting.posts.domain
|
||||
|
||||
import android.net.Uri
|
||||
import com.isolaatti.images.common.domain.entity.LocalImage
|
||||
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||
@ -18,7 +19,7 @@ interface PostsRepository {
|
||||
|
||||
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<MutableList<Post>>>
|
||||
|
||||
fun makePost(createPostDto: CreatePostDto, images: List<Uri>): Flow<Resource<PostingSteps>>
|
||||
fun makePost(createPostDto: CreatePostDto, images: List<LocalImage>): Flow<Resource<PostingSteps>>
|
||||
fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>>
|
||||
fun deletePost(postId: Long): Flow<Resource<PostDeletedDto>>
|
||||
fun loadPost(postId: Long): Flow<Resource<Post>>
|
||||
|
||||
@ -5,7 +5,7 @@ import android.os.Parcelable
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.common.Ownable
|
||||
import com.isolaatti.common.hashtagRegex
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||
|
||||
data class Post(
|
||||
@ -22,7 +22,7 @@ data class Post(
|
||||
val squadName: String?,
|
||||
var liked: Boolean,
|
||||
val audio: Audio? = null,
|
||||
val images: List<Image>
|
||||
val images: List<RemoteImage>
|
||||
) : Ownable, Parcelable {
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readLong(),
|
||||
@ -38,7 +38,7 @@ data class Post(
|
||||
parcel.readString(),
|
||||
parcel.readByte() != 0.toByte(),
|
||||
parcel.readSerializable() as? Audio,
|
||||
parcel.readParcelableArray(Image::class.java.classLoader)?.toList() as? List<Image> ?: emptyList()
|
||||
parcel.readParcelableArray(RemoteImage::class.java.classLoader)?.toList() as? List<RemoteImage> ?: emptyList()
|
||||
) {
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ data class Post(
|
||||
squadName = it.squadName,
|
||||
liked = it.liked,
|
||||
audio = it.audio?.let { audioDto -> Audio.fromDto(audioDto) },
|
||||
images = it.post.images.map { imageDto -> Image(imageDto.id) }
|
||||
images = it.post.images.map { imageDto -> RemoteImage(imageDto.id) }
|
||||
)
|
||||
}.toMutableList()
|
||||
}
|
||||
@ -78,7 +78,7 @@ data class Post(
|
||||
userName = postDto.userName,
|
||||
squadName = postDto.squadName,
|
||||
liked = postDto.liked,
|
||||
images = postDto.post.images.map { imageDto -> Image(imageDto.id) }
|
||||
images = postDto.post.images.map { imageDto -> RemoteImage(imageDto.id) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.isolaatti.posting.posts.domain.use_case
|
||||
|
||||
import android.net.Uri
|
||||
import com.isolaatti.images.common.domain.entity.LocalImage
|
||||
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
||||
import com.isolaatti.posting.posts.domain.PostingSteps
|
||||
import com.isolaatti.posting.posts.domain.PostsRepository
|
||||
@ -12,7 +13,7 @@ class MakePost @Inject constructor(private val postsRepository: PostsRepository)
|
||||
operator fun invoke(
|
||||
privacy: Int,
|
||||
content: String,
|
||||
images: List<Uri>,
|
||||
images: List<LocalImage>,
|
||||
audioId: String?,
|
||||
squadId: String?
|
||||
): Flow<Resource<PostingSteps>> {
|
||||
|
||||
@ -2,23 +2,27 @@ package com.isolaatti.posting.posts.presentation
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.LocalImage
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
||||
import com.isolaatti.posting.posts.data.remote.EditPostDto.Companion.PRIVACY_ISOLAATTI
|
||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||
import com.isolaatti.posting.posts.domain.PostingSteps
|
||||
import com.isolaatti.posting.posts.domain.entity.Post
|
||||
import com.isolaatti.posting.posts.domain.use_case.EditPost
|
||||
import com.isolaatti.posting.posts.domain.use_case.LoadSinglePost
|
||||
import com.isolaatti.posting.posts.domain.use_case.MakePost
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@ -44,8 +48,9 @@ class CreatePostViewModel @Inject constructor(
|
||||
val sendingPost: MutableLiveData<Boolean?> = MutableLiveData(null)
|
||||
val postToEdit: MutableLiveData<EditPostDto> = MutableLiveData()
|
||||
val liveContent: MutableLiveData<String> = MutableLiveData()
|
||||
private val _photos: MutableStateFlow<List<Uri>> = MutableStateFlow(emptyList())
|
||||
val photos: StateFlow<List<Uri>> get() = _photos
|
||||
private val _photos: MutableStateFlow<List<Image>> = MutableStateFlow(emptyList())
|
||||
val photos: StateFlow<List<Image>> get() = _photos
|
||||
var audioToUpload: Uri? = null
|
||||
|
||||
private val _postingStep = MutableStateFlow(PostingSteps.Unspecified)
|
||||
val postingStep: StateFlow<PostingSteps> get() = _postingStep
|
||||
@ -55,6 +60,15 @@ class CreatePostViewModel @Inject constructor(
|
||||
private var audioDraft: Long? = null
|
||||
private var audioId: String? = null
|
||||
|
||||
// region Player state
|
||||
val isPlaying: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
var position: MutableStateFlow<Long> = MutableStateFlow(0L)
|
||||
var duration: MutableStateFlow<Long> = MutableStateFlow(0L)
|
||||
var isLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
var ended: Boolean = false
|
||||
// endregion
|
||||
|
||||
|
||||
/**
|
||||
* postDiscussion() and editDiscussion() will check for audios pending to upload (drafts). It will
|
||||
* upload it (if any) and then send the request to post.
|
||||
@ -66,7 +80,7 @@ class CreatePostViewModel @Inject constructor(
|
||||
makePost(
|
||||
privacy = EditPostDto.PRIVACY_ISOLAATTI,
|
||||
content = liveContent.value ?: "",
|
||||
images = photos.value,
|
||||
images = photos.value.filterIsInstance<LocalImage>(),
|
||||
audioId = audioId,
|
||||
squadId = null
|
||||
).onEach {
|
||||
@ -140,6 +154,8 @@ class CreatePostViewModel @Inject constructor(
|
||||
//postToEdit.postValue(EditPostDto(PRIVACY_ISOLAATTI, content = it.textContent, postId = it.id))
|
||||
//it.audio?.let { audio -> audioAttachment.postValue(audio) }
|
||||
|
||||
_photos.value = it.images
|
||||
|
||||
liveContent.postValue(it.textContent)
|
||||
}
|
||||
}
|
||||
@ -158,13 +174,26 @@ class CreatePostViewModel @Inject constructor(
|
||||
audioAttachment.value = null
|
||||
}
|
||||
|
||||
fun addPicture(uri: Uri) {
|
||||
fun addPicture(uri: LocalImage) {
|
||||
_photos.value = listOf(uri) + _photos.value
|
||||
}
|
||||
|
||||
fun removePicture(index: Int) {
|
||||
val image = _photos.value.getOrNull(index)
|
||||
if(image == null) {
|
||||
return
|
||||
}
|
||||
|
||||
when(image) {
|
||||
is LocalImage -> {
|
||||
_photos.value = _photos.value.toMutableList().apply {
|
||||
removeAt(index)
|
||||
}
|
||||
}
|
||||
is RemoteImage -> {
|
||||
image.delete = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,13 @@
|
||||
package com.isolaatti.posting.posts.presentation
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import coil.load
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
|
||||
class PostImagesViewPagerAdapter(private val images: List<Image>) :
|
||||
class PostImagesViewPagerAdapter(private val images: List<RemoteImage>) :
|
||||
RecyclerView.Adapter<PostImagesViewPagerAdapter.PostImagesViewPagerAdapterItemViewHolder>() {
|
||||
class PostImagesViewPagerAdapterItemViewHolder(val imageView: ImageView) : ViewHolder(imageView)
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
package com.isolaatti.posting.posts.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.media.MediaRecorder
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
@ -9,9 +12,6 @@ import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.gestures.ScrollableState
|
||||
import androidx.compose.foundation.gestures.rememberScrollableState
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -21,7 +21,6 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@ -47,21 +46,33 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.isolaatti.MyApplication
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.common.components.AudioRecorder
|
||||
import com.isolaatti.audio.player.MediaService
|
||||
import com.isolaatti.common.IsolaattiBaseActivity
|
||||
import com.isolaatti.common.IsolaattiTheme
|
||||
import com.isolaatti.images.common.domain.entity.LocalImage
|
||||
import com.isolaatti.posting.posts.components.PostAttachments
|
||||
import com.isolaatti.posting.posts.domain.PostingSteps
|
||||
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.guava.await
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
@ -87,7 +98,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
|
||||
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
||||
if(it != null) {
|
||||
viewModel.addPicture(it)
|
||||
viewModel.addPicture(LocalImage(it))
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,9 +111,37 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
|
||||
private val takePhotoLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) {
|
||||
if(it && cameraPhotoUri != null) {
|
||||
viewModel.addPicture(cameraPhotoUri!!)
|
||||
viewModel.addPicture(LocalImage(cameraPhotoUri!!))
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var mediaControllerFuture: ListenableFuture<MediaController>
|
||||
|
||||
private lateinit var mediaController: MediaController
|
||||
private lateinit var mediaRecorder: MediaRecorder
|
||||
|
||||
private val playerListener: Player.Listener = object: Player.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
viewModel.isPlaying.value = isPlaying
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
when(playbackState) {
|
||||
Player.STATE_READY -> {
|
||||
viewModel.ended = false
|
||||
viewModel.isLoading.value = false
|
||||
viewModel.duration.value = this@CreatePostActivity.mediaController.duration
|
||||
}
|
||||
Player.STATE_ENDED -> {
|
||||
viewModel.ended = true
|
||||
}
|
||||
Player.STATE_BUFFERING -> {
|
||||
viewModel.isLoading.value = true
|
||||
}
|
||||
|
||||
Player.STATE_IDLE -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@ -110,6 +149,16 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val sessionToken = SessionToken(this, ComponentName(this, MediaService::class.java))
|
||||
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
||||
|
||||
mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(this) else MediaRecorder()
|
||||
|
||||
lifecycleScope.launch {
|
||||
mediaController = mediaControllerFuture.await()
|
||||
|
||||
mediaController.addListener(playerListener)
|
||||
|
||||
setContent {
|
||||
val text by viewModel.liveContent.observeAsState("")
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@ -119,9 +168,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
mode == EXTRA_MODE_EDIT && postId != 0L
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
|
||||
IsolaattiTheme {
|
||||
Scaffold(
|
||||
@ -158,6 +205,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val posting by viewModel.sendingPost.observeAsState()
|
||||
var audioRecorderIsVisible by remember { mutableStateOf(false) }
|
||||
|
||||
if(posting == true) {
|
||||
val postingStep by viewModel.postingStep.collectAsState()
|
||||
@ -187,6 +235,9 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
finish()
|
||||
}
|
||||
Column(modifier = Modifier.padding(it).verticalScroll(scrollState)) {
|
||||
|
||||
|
||||
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = { textFieldValue ->
|
||||
@ -203,21 +254,93 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
unfocusedBorderColor = Color.Transparent
|
||||
),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {}) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
audioRecorderIsVisible = !audioRecorderIsVisible
|
||||
}
|
||||
) {
|
||||
Icon(painterResource(id = R.drawable.baseline_mic_24), null)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(false) {
|
||||
AudioRecorder(
|
||||
onAudioRecorded = {
|
||||
|
||||
},
|
||||
dismissible = true,
|
||||
onDismiss = {
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
AnimatedVisibility(audioRecorderIsVisible) {
|
||||
|
||||
val isPlaying by viewModel.isPlaying.collectAsState()
|
||||
val position by viewModel.position.collectAsState()
|
||||
val duration by viewModel.duration.collectAsState()
|
||||
val isLoading by viewModel.isLoading.collectAsState()
|
||||
|
||||
val outputFile = File(this@CreatePostActivity.filesDir, "${System.currentTimeMillis()}.3gp")
|
||||
|
||||
LaunchedEffect(isPlaying) {
|
||||
while(isPlaying) {
|
||||
delay(500.milliseconds)
|
||||
viewModel.position.value = mediaController.currentPosition
|
||||
}
|
||||
}
|
||||
|
||||
AudioRecorder(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
onDismiss = {
|
||||
audioRecorderIsVisible = false
|
||||
},
|
||||
onPlay = {
|
||||
if(mediaController.isCommandAvailable(Player.COMMAND_PREPARE)) {
|
||||
mediaController.prepare()
|
||||
}
|
||||
|
||||
if(viewModel.ended) {
|
||||
mediaController.seekTo(0L)
|
||||
}
|
||||
mediaController.playWhenReady = true
|
||||
},
|
||||
onPause = {
|
||||
mediaController.pause()
|
||||
},
|
||||
onStartRecording = {
|
||||
|
||||
mediaRecorder.run {
|
||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
|
||||
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
setOutputFile(outputFile)
|
||||
} else {
|
||||
setOutputFile(outputFile.path)
|
||||
}
|
||||
|
||||
prepare()
|
||||
start()
|
||||
}
|
||||
},
|
||||
onPauseRecording = {
|
||||
mediaRecorder.pause()
|
||||
},
|
||||
onStopRecording = {
|
||||
mediaRecorder.stop()
|
||||
val audioUri = outputFile.toUri()
|
||||
mediaController.setMediaItem(
|
||||
MediaItem.Builder()
|
||||
.setUri(audioUri)
|
||||
.setMediaMetadata(MediaMetadata.Builder()
|
||||
.setTitle(getString(R.string.just_recorded_audio_title))
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
viewModel.audioToUpload = audioUri
|
||||
},
|
||||
isPlaying = isPlaying,
|
||||
position = position,
|
||||
durationSeconds = duration,
|
||||
isRecording = false,
|
||||
recordIsPaused = false,
|
||||
recordIsStopped = false,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
|
||||
@ -250,22 +373,26 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
intent.extras?.let {
|
||||
mode = it.getInt(EXTRA_KEY_MODE, EXTRA_MODE_CREATE)
|
||||
postId = it.getLong(EXTRA_KEY_POST_ID)
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
|
||||
viewModel.loadDiscussion(postId)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun exit() {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
MediaController.releaseFuture(mediaControllerFuture)
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ package com.isolaatti.profile.data.repository
|
||||
import android.util.Log
|
||||
import com.isolaatti.auth.data.local.UserInfoDao
|
||||
import com.isolaatti.auth.data.local.UserInfoEntity
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.profile.data.remote.ProfileApi
|
||||
import com.isolaatti.profile.data.remote.UpdateProfileDto
|
||||
import com.isolaatti.profile.domain.ProfileRepository
|
||||
@ -37,7 +37,7 @@ class ProfileRepositoryImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun setProfileImage(image: Image): Flow<Resource<Boolean>> = flow {
|
||||
override fun setProfileImage(image: RemoteImage): Flow<Resource<Boolean>> = flow {
|
||||
|
||||
try {
|
||||
val result = profileApi.setProfileImage(image.id).awaitResponse()
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.isolaatti.profile.domain
|
||||
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.profile.data.remote.UpdateProfileDto
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.profile.domain.entity.UserProfile
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -9,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
interface ProfileRepository {
|
||||
fun getProfile(userId: Int): Flow<Resource<UserProfile>>
|
||||
|
||||
fun setProfileImage(image: Image): Flow<Resource<Boolean>>
|
||||
fun setProfileImage(image: RemoteImage): Flow<Resource<Boolean>>
|
||||
|
||||
fun updateProfile(newDisplayName: String, newDescription: String): Flow<Resource<Boolean>>
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
package com.isolaatti.profile.domain.use_case
|
||||
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.profile.domain.ProfileRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class SetProfileImage @Inject constructor(private val profileRepository: ProfileRepository) {
|
||||
operator fun invoke(image: Image): Flow<Resource<Boolean>> = profileRepository.setProfileImage(image)
|
||||
operator fun invoke(image: RemoteImage): Flow<Resource<Boolean>> = profileRepository.setProfileImage(image)
|
||||
}
|
||||
@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.followers.domain.FollowingState
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.posting.posts.presentation.PostListingViewModelBase
|
||||
import com.isolaatti.posting.posts.presentation.UpdateEvent
|
||||
import com.isolaatti.profile.domain.entity.UserProfile
|
||||
@ -83,7 +83,7 @@ class ProfileViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setProfileImage(image: Image) {
|
||||
fun setProfileImage(image: RemoteImage) {
|
||||
viewModelScope.launch {
|
||||
setProfileImageUC(image).onEach {
|
||||
_profile.postValue(_profile.value?.copy(profileImageId = image.id))
|
||||
|
||||
@ -8,7 +8,6 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
@ -23,7 +22,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.isolaatti.BuildConfig
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.common.CoilImageLoader.imageLoader
|
||||
import com.isolaatti.common.Dialogs
|
||||
import com.isolaatti.common.ErrorMessageViewModel
|
||||
@ -35,7 +33,7 @@ import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragme
|
||||
import com.isolaatti.databinding.FragmentDiscussionsBinding
|
||||
import com.isolaatti.followers.domain.FollowingState
|
||||
import com.isolaatti.hashtags.ui.HashtagsPostsActivity
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
|
||||
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
|
||||
import com.isolaatti.posting.posts.domain.entity.Post
|
||||
@ -182,7 +180,7 @@ class ProfileMainFragment : Fragment() {
|
||||
val profilePictureUrl = profile?.profilePictureUrl
|
||||
if(profilePictureUrl != null) {
|
||||
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(
|
||||
Image(profile.profileImageId ?: "")
|
||||
RemoteImage(profile.profileImageId ?: "")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
package com.isolaatti.utils
|
||||
|
||||
fun Int.clockFormat(): String {
|
||||
val minutes = this / 60
|
||||
val seconds = this % 60
|
||||
import java.lang.StringBuilder
|
||||
import kotlin.time.Duration
|
||||
|
||||
return "${minutes}:${seconds.toString().padStart(2, '0')}"
|
||||
fun Duration.clockFormat(): String {
|
||||
return toComponents { hours, minutes, seconds, nanoseconds ->
|
||||
val sb = StringBuilder().apply {
|
||||
if(hours > 0) {
|
||||
append(hours.toString().padStart(2, '0'))
|
||||
append(":")
|
||||
}
|
||||
append(minutes.toString().padStart(2, '0'))
|
||||
append(":")
|
||||
append(seconds.toString().padStart(2, '0'))
|
||||
}
|
||||
|
||||
sb.toString()
|
||||
}
|
||||
}
|
||||
@ -220,6 +220,12 @@
|
||||
<string name="invalid_arg">Invalid value passed</string>
|
||||
<string name="posting">Posting</string>
|
||||
<string name="uploading_photos">Uploading photos</string>
|
||||
<string name="record_audio_permission_required">We need permission to use your microphone</string>
|
||||
<string name="request_permission">Request permission</string>
|
||||
<string name="record_audio_permission_required_rationale">In order to be able to record we need to access your mic.</string>
|
||||
<string name="pause_audio">Pause audio</string>
|
||||
<string name="play_audio">Play audio</string>
|
||||
<string name="just_recorded_audio_title">Just recorded audio</string>
|
||||
<string-array name="report_reasons">
|
||||
<item>Spam</item>
|
||||
<item>Explicit content</item>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user