WIP
This commit is contained in:
parent
031722920a
commit
77d1cc13b4
@ -58,6 +58,7 @@ dependencies {
|
|||||||
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
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.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-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 'androidx.compose.runtime:runtime-livedata'
|
||||||
|
|
||||||
implementation "io.coil-kt.coil3:coil-compose:3.0.1"
|
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.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<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
|
<application
|
||||||
android:name=".MyApplication"
|
android:name=".MyApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
@ -17,6 +19,7 @@
|
|||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
|
||||||
|
<!-- Activities -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||||
android:theme="@style/Theme.Isolaatti.OSS" />
|
android:theme="@style/Theme.Isolaatti.OSS" />
|
||||||
@ -48,10 +51,12 @@
|
|||||||
<activity android:name=".about.AboutActivity" android:theme="@style/Theme.Isolaatti"/>
|
<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=".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=".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=".profile.ui.EditProfileActivity" android:theme="@style/Theme.Isolaatti" />
|
||||||
<activity android:name=".posting.posts.ui.PostInfoActivity" 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" />
|
<activity android:name=".hashtags.ui.HashtagsPostsActivity" android:theme="@style/Theme.Isolaatti" />
|
||||||
|
|
||||||
|
<!-- End activities-->
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:authorities="com.isolaatti.provider"
|
android:authorities="com.isolaatti.provider"
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
@ -60,6 +65,8 @@
|
|||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths"/>
|
android:resource="@xml/provider_paths"/>
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<!-- Services -->
|
||||||
<service android:name=".push_notifications.FcmService"
|
<service android:name=".push_notifications.FcmService"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@ -67,6 +74,13 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</service>
|
</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>
|
</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
|
package com.isolaatti.audio.common.components
|
||||||
|
|
||||||
import android.media.MediaRecorder
|
import android.Manifest
|
||||||
import android.net.Uri
|
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.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.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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
|
@Composable
|
||||||
fun AudioRecorder(
|
fun AudioRecorder(
|
||||||
onAudioRecorded: (Uri) -> Unit,
|
onDismiss: () -> Unit = {},
|
||||||
dismissible: Boolean,
|
onPlay: () -> Unit = {},
|
||||||
onDismiss: () -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun AudioRecorderPreview() {
|
|
||||||
|
|
||||||
}
|
|
||||||
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.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.isolaatti.images.common.data.dao.ImagesDraftsDao
|
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.DeleteImagesDto
|
||||||
import com.isolaatti.images.common.data.remote.ImagesApi
|
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.images.common.domain.repository.ImagesRepository
|
||||||
import com.isolaatti.utils.Resource
|
import com.isolaatti.utils.Resource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -28,13 +27,13 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
|
|||||||
companion object {
|
companion object {
|
||||||
const val TAG = "ImagesRepositoryImpl"
|
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())
|
emit(Resource.Loading())
|
||||||
try {
|
try {
|
||||||
val response = imagesApi.getImagesOfUser(userId, lastId).awaitResponse()
|
val response = imagesApi.getImagesOfUser(userId, lastId).awaitResponse()
|
||||||
if(response.isSuccessful) {
|
if(response.isSuccessful) {
|
||||||
val imagesDto = response.body()
|
val imagesDto = response.body()
|
||||||
val images = imagesDto?.data?.map { Image(it.id) }
|
val images = imagesDto?.data?.map { RemoteImage(it.id) }
|
||||||
|
|
||||||
emit(Resource.Success(images))
|
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())
|
emit(Resource.Loading())
|
||||||
val dto = DeleteImagesDto(images.map { it.id })
|
val dto = DeleteImagesDto(images.map { it.id })
|
||||||
try {
|
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())
|
emit(Resource.Loading())
|
||||||
var imageInputStream: InputStream? = null
|
var imageInputStream: InputStream? = null
|
||||||
try {
|
try {
|
||||||
@ -93,7 +92,7 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
|
|||||||
emit(Resource.Error(Resource.Error.ErrorType.ServerError))
|
emit(Resource.Error(Resource.Error.ErrorType.ServerError))
|
||||||
return@flow
|
return@flow
|
||||||
}
|
}
|
||||||
val image = Image(imageDto.id)
|
val image = RemoteImage(imageDto.id)
|
||||||
emit(Resource.Success(image))
|
emit(Resource.Success(image))
|
||||||
} else {
|
} else {
|
||||||
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
|
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
|
||||||
|
|||||||
@ -1,56 +1,6 @@
|
|||||||
package com.isolaatti.images.common.domain.entity
|
package com.isolaatti.images.common.domain.entity
|
||||||
|
|
||||||
import android.os.Parcel
|
/**
|
||||||
import android.os.Parcelable
|
* Marker interface to identify images
|
||||||
import com.isolaatti.common.Deletable
|
*/
|
||||||
import com.isolaatti.images.common.data.remote.ImageDto
|
interface Image
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
package com.isolaatti.images.common.domain.repository
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.isolaatti.images.common.data.entity.ImageDraftEntity
|
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||||
import com.isolaatti.images.common.domain.entity.Image
|
|
||||||
import com.isolaatti.utils.Resource
|
import com.isolaatti.utils.Resource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface ImagesRepository {
|
interface ImagesRepository {
|
||||||
fun getImagesOfUser(userId: Int, lastId: String? = null): Flow<Resource<List<Image>>>
|
fun getImagesOfUser(userId: Int, lastId: String? = null): Flow<Resource<List<RemoteImage>>>
|
||||||
fun deleteImages(images: List<Image>): Flow<Resource<Boolean>>
|
fun deleteImages(images: List<RemoteImage>): Flow<Resource<Boolean>>
|
||||||
fun uploadImage(imageUri: Uri, postId: Long, squadId: String?): Flow<Resource<Image>>
|
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.fragment.app.Fragment
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
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
|
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 {
|
override fun getItemCount(): Int {
|
||||||
return images.size
|
return images.size
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,7 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.isolaatti.databinding.ActivityPictureViewerBinding
|
import com.isolaatti.databinding.ActivityPictureViewerBinding
|
||||||
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 PictureViewerActivity : AppCompatActivity() {
|
class PictureViewerActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityPictureViewerBinding
|
private lateinit var binding: ActivityPictureViewerBinding
|
||||||
@ -25,7 +24,7 @@ class PictureViewerActivity : AppCompatActivity() {
|
|||||||
const val EXTRA_IMAGES = "images"
|
const val EXTRA_IMAGES = "images"
|
||||||
const val EXTRA_IMAGE_POSITiON = "position"
|
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()) {
|
if(images.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,14 +8,14 @@ import androidx.fragment.app.Fragment
|
|||||||
import coil.load
|
import coil.load
|
||||||
import com.isolaatti.common.CoilImageLoader.imageLoader
|
import com.isolaatti.common.CoilImageLoader.imageLoader
|
||||||
import com.isolaatti.databinding.FragmentTouchImageViewWrapperBinding
|
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
|
import com.ortiz.touchview.OnTouchImageViewListener
|
||||||
|
|
||||||
|
|
||||||
class PictureViewerImageWrapperFragment : Fragment() {
|
class PictureViewerImageWrapperFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentTouchImageViewWrapperBinding
|
private lateinit var binding: FragmentTouchImageViewWrapperBinding
|
||||||
private var image: Image? = null
|
private var image: RemoteImage? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -24,7 +24,7 @@ class PictureViewerImageWrapperFragment : Fragment() {
|
|||||||
): View {
|
): View {
|
||||||
binding = FragmentTouchImageViewWrapperBinding.inflate(inflater)
|
binding = FragmentTouchImageViewWrapperBinding.inflate(inflater)
|
||||||
|
|
||||||
image = arguments?.getSerializable(ARGUMENT_IMAGE) as Image
|
image = arguments?.getSerializable(ARGUMENT_IMAGE) as RemoteImage
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ class PictureViewerImageWrapperFragment : Fragment() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ARGUMENT_IMAGE = "image"
|
const val ARGUMENT_IMAGE = "image"
|
||||||
fun getInstance(image: Image): PictureViewerImageWrapperFragment {
|
fun getInstance(image: RemoteImage): PictureViewerImageWrapperFragment {
|
||||||
val fragment = PictureViewerImageWrapperFragment()
|
val fragment = PictureViewerImageWrapperFragment()
|
||||||
fragment.arguments = Bundle().apply {
|
fragment.arguments = Bundle().apply {
|
||||||
putParcelable(ARGUMENT_IMAGE, image)
|
putParcelable(ARGUMENT_IMAGE, image)
|
||||||
|
|||||||
@ -7,14 +7,14 @@ import android.view.ViewGroup
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.isolaatti.databinding.FragmentMainPictureViewerBinding
|
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
|
import com.isolaatti.images.picture_viewer.presentation.PictureViewerViewPagerAdapter
|
||||||
|
|
||||||
class PictureViewerMainFragment : Fragment() {
|
class PictureViewerMainFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentMainPictureViewerBinding
|
private lateinit var binding: FragmentMainPictureViewerBinding
|
||||||
|
|
||||||
lateinit var images: Array<Image>
|
lateinit var images: Array<RemoteImage>
|
||||||
|
|
||||||
private val onPageChangeCallback = object: ViewPager2.OnPageChangeCallback() {
|
private val onPageChangeCallback = object: ViewPager2.OnPageChangeCallback() {
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
@ -36,7 +36,7 @@ class PictureViewerMainFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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 position = requireActivity().intent.extras?.getInt(PictureViewerActivity.EXTRA_IMAGE_POSITiON) ?: 0
|
||||||
val adapter = PictureViewerViewPagerAdapter(this, images)
|
val adapter = PictureViewerViewPagerAdapter(this, images)
|
||||||
binding.viewpager.adapter = adapter
|
binding.viewpager.adapter = adapter
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.isolaatti.posting.posts.components
|
package com.isolaatti.posting.posts.components
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@ -33,11 +34,14 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.isolaatti.R
|
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
|
@Composable
|
||||||
fun PostAttachments(
|
fun PostAttachments(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
images: List<Uri>,
|
images: List<Image>,
|
||||||
deletable: Boolean,
|
deletable: Boolean,
|
||||||
imageAddable: Boolean,
|
imageAddable: Boolean,
|
||||||
onImageClick: (index: Int) -> Unit,
|
onImageClick: (index: Int) -> Unit,
|
||||||
@ -103,14 +107,27 @@ fun PostAttachments(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when(val image = images[index]) {
|
||||||
|
is LocalImage -> {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = images[index],
|
model = image.uri,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.fillMaxSize().zIndex(1f),
|
modifier = Modifier.fillMaxSize().zIndex(1f),
|
||||||
contentScale = ContentScale.Crop
|
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.google.gson.Gson
|
||||||
import com.isolaatti.images.common.data.remote.ImageDto
|
import com.isolaatti.images.common.data.remote.ImageDto
|
||||||
import com.isolaatti.images.common.data.remote.ImagesApi
|
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.CreatePostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.DeletePostDto
|
import com.isolaatti.posting.posts.data.remote.DeletePostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
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())
|
emit(Resource.Loading())
|
||||||
try {
|
try {
|
||||||
val hasImages = images.isNotEmpty()
|
val hasImages = images.isNotEmpty()
|
||||||
@ -103,10 +104,10 @@ class PostsRepositoryImpl @Inject constructor(
|
|||||||
return@flow
|
return@flow
|
||||||
}
|
}
|
||||||
emit(Resource.Success(PostingSteps.UploadingPhotos))
|
emit(Resource.Success(PostingSteps.UploadingPhotos))
|
||||||
images.forEach { imageUri ->
|
images.forEach { localImage ->
|
||||||
var imageInputStream: InputStream? = null
|
var imageInputStream: InputStream? = null
|
||||||
try {
|
try {
|
||||||
imageInputStream = contentResolver.openInputStream(imageUri)
|
imageInputStream = contentResolver.openInputStream(localImage.uri)
|
||||||
val bitmap = BitmapFactory.decodeStream(imageInputStream)
|
val bitmap = BitmapFactory.decodeStream(imageInputStream)
|
||||||
|
|
||||||
if(bitmap == null) {
|
if(bitmap == null) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.isolaatti.posting.posts.domain
|
package com.isolaatti.posting.posts.domain
|
||||||
|
|
||||||
import android.net.Uri
|
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.CreatePostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
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 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 editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>>
|
||||||
fun deletePost(postId: Long): Flow<Resource<PostDeletedDto>>
|
fun deletePost(postId: Long): Flow<Resource<PostDeletedDto>>
|
||||||
fun loadPost(postId: Long): Flow<Resource<Post>>
|
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.audio.common.domain.Audio
|
||||||
import com.isolaatti.common.Ownable
|
import com.isolaatti.common.Ownable
|
||||||
import com.isolaatti.common.hashtagRegex
|
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
|
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||||
|
|
||||||
data class Post(
|
data class Post(
|
||||||
@ -22,7 +22,7 @@ data class Post(
|
|||||||
val squadName: String?,
|
val squadName: String?,
|
||||||
var liked: Boolean,
|
var liked: Boolean,
|
||||||
val audio: Audio? = null,
|
val audio: Audio? = null,
|
||||||
val images: List<Image>
|
val images: List<RemoteImage>
|
||||||
) : Ownable, Parcelable {
|
) : Ownable, Parcelable {
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readLong(),
|
parcel.readLong(),
|
||||||
@ -38,7 +38,7 @@ data class Post(
|
|||||||
parcel.readString(),
|
parcel.readString(),
|
||||||
parcel.readByte() != 0.toByte(),
|
parcel.readByte() != 0.toByte(),
|
||||||
parcel.readSerializable() as? Audio,
|
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,
|
squadName = it.squadName,
|
||||||
liked = it.liked,
|
liked = it.liked,
|
||||||
audio = it.audio?.let { audioDto -> Audio.fromDto(audioDto) },
|
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()
|
}.toMutableList()
|
||||||
}
|
}
|
||||||
@ -78,7 +78,7 @@ data class Post(
|
|||||||
userName = postDto.userName,
|
userName = postDto.userName,
|
||||||
squadName = postDto.squadName,
|
squadName = postDto.squadName,
|
||||||
liked = postDto.liked,
|
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
|
package com.isolaatti.posting.posts.domain.use_case
|
||||||
|
|
||||||
import android.net.Uri
|
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.CreatePostDto
|
||||||
import com.isolaatti.posting.posts.domain.PostingSteps
|
import com.isolaatti.posting.posts.domain.PostingSteps
|
||||||
import com.isolaatti.posting.posts.domain.PostsRepository
|
import com.isolaatti.posting.posts.domain.PostsRepository
|
||||||
@ -12,7 +13,7 @@ class MakePost @Inject constructor(private val postsRepository: PostsRepository)
|
|||||||
operator fun invoke(
|
operator fun invoke(
|
||||||
privacy: Int,
|
privacy: Int,
|
||||||
content: String,
|
content: String,
|
||||||
images: List<Uri>,
|
images: List<LocalImage>,
|
||||||
audioId: String?,
|
audioId: String?,
|
||||||
squadId: String?
|
squadId: String?
|
||||||
): Flow<Resource<PostingSteps>> {
|
): Flow<Resource<PostingSteps>> {
|
||||||
|
|||||||
@ -2,23 +2,27 @@ package com.isolaatti.posting.posts.presentation
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
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.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.isolaatti.audio.common.domain.Audio
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
import com.isolaatti.audio.common.domain.Playable
|
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
|
||||||
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.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.EditPost
|
||||||
import com.isolaatti.posting.posts.domain.use_case.LoadSinglePost
|
import com.isolaatti.posting.posts.domain.use_case.LoadSinglePost
|
||||||
import com.isolaatti.posting.posts.domain.use_case.MakePost
|
import com.isolaatti.posting.posts.domain.use_case.MakePost
|
||||||
import com.isolaatti.utils.Resource
|
import com.isolaatti.utils.Resource
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@ -44,8 +48,9 @@ class CreatePostViewModel @Inject constructor(
|
|||||||
val sendingPost: MutableLiveData<Boolean?> = MutableLiveData(null)
|
val sendingPost: MutableLiveData<Boolean?> = MutableLiveData(null)
|
||||||
val postToEdit: MutableLiveData<EditPostDto> = MutableLiveData()
|
val postToEdit: MutableLiveData<EditPostDto> = MutableLiveData()
|
||||||
val liveContent: MutableLiveData<String> = MutableLiveData()
|
val liveContent: MutableLiveData<String> = MutableLiveData()
|
||||||
private val _photos: MutableStateFlow<List<Uri>> = MutableStateFlow(emptyList())
|
private val _photos: MutableStateFlow<List<Image>> = MutableStateFlow(emptyList())
|
||||||
val photos: StateFlow<List<Uri>> get() = _photos
|
val photos: StateFlow<List<Image>> get() = _photos
|
||||||
|
var audioToUpload: Uri? = null
|
||||||
|
|
||||||
private val _postingStep = MutableStateFlow(PostingSteps.Unspecified)
|
private val _postingStep = MutableStateFlow(PostingSteps.Unspecified)
|
||||||
val postingStep: StateFlow<PostingSteps> get() = _postingStep
|
val postingStep: StateFlow<PostingSteps> get() = _postingStep
|
||||||
@ -55,6 +60,15 @@ class CreatePostViewModel @Inject constructor(
|
|||||||
private var audioDraft: Long? = null
|
private var audioDraft: Long? = null
|
||||||
private var audioId: String? = 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
|
* postDiscussion() and editDiscussion() will check for audios pending to upload (drafts). It will
|
||||||
* upload it (if any) and then send the request to post.
|
* upload it (if any) and then send the request to post.
|
||||||
@ -66,7 +80,7 @@ class CreatePostViewModel @Inject constructor(
|
|||||||
makePost(
|
makePost(
|
||||||
privacy = EditPostDto.PRIVACY_ISOLAATTI,
|
privacy = EditPostDto.PRIVACY_ISOLAATTI,
|
||||||
content = liveContent.value ?: "",
|
content = liveContent.value ?: "",
|
||||||
images = photos.value,
|
images = photos.value.filterIsInstance<LocalImage>(),
|
||||||
audioId = audioId,
|
audioId = audioId,
|
||||||
squadId = null
|
squadId = null
|
||||||
).onEach {
|
).onEach {
|
||||||
@ -140,6 +154,8 @@ class CreatePostViewModel @Inject constructor(
|
|||||||
//postToEdit.postValue(EditPostDto(PRIVACY_ISOLAATTI, content = it.textContent, postId = it.id))
|
//postToEdit.postValue(EditPostDto(PRIVACY_ISOLAATTI, content = it.textContent, postId = it.id))
|
||||||
//it.audio?.let { audio -> audioAttachment.postValue(audio) }
|
//it.audio?.let { audio -> audioAttachment.postValue(audio) }
|
||||||
|
|
||||||
|
_photos.value = it.images
|
||||||
|
|
||||||
liveContent.postValue(it.textContent)
|
liveContent.postValue(it.textContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,13 +174,26 @@ class CreatePostViewModel @Inject constructor(
|
|||||||
audioAttachment.value = null
|
audioAttachment.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addPicture(uri: Uri) {
|
fun addPicture(uri: LocalImage) {
|
||||||
_photos.value = listOf(uri) + _photos.value
|
_photos.value = listOf(uri) + _photos.value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removePicture(index: Int) {
|
fun removePicture(index: Int) {
|
||||||
|
val image = _photos.value.getOrNull(index)
|
||||||
|
if(image == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when(image) {
|
||||||
|
is LocalImage -> {
|
||||||
_photos.value = _photos.value.toMutableList().apply {
|
_photos.value = _photos.value.toMutableList().apply {
|
||||||
removeAt(index)
|
removeAt(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is RemoteImage -> {
|
||||||
|
image.delete = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,16 +1,13 @@
|
|||||||
package com.isolaatti.posting.posts.presentation
|
package com.isolaatti.posting.posts.presentation
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RelativeLayout
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import coil.load
|
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>() {
|
RecyclerView.Adapter<PostImagesViewPagerAdapter.PostImagesViewPagerAdapterItemViewHolder>() {
|
||||||
class PostImagesViewPagerAdapterItemViewHolder(val imageView: ImageView) : ViewHolder(imageView)
|
class PostImagesViewPagerAdapterItemViewHolder(val imageView: ImageView) : ViewHolder(imageView)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
package com.isolaatti.posting.posts.ui
|
package com.isolaatti.posting.posts.ui
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.media.MediaRecorder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
@ -9,9 +12,6 @@ import androidx.activity.result.PickVisualMediaRequest
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.BasicAlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
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.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.MyApplication
|
||||||
import com.isolaatti.R
|
import com.isolaatti.R
|
||||||
import com.isolaatti.audio.common.components.AudioRecorder
|
import com.isolaatti.audio.common.components.AudioRecorder
|
||||||
|
import com.isolaatti.audio.player.MediaService
|
||||||
import com.isolaatti.common.IsolaattiBaseActivity
|
import com.isolaatti.common.IsolaattiBaseActivity
|
||||||
import com.isolaatti.common.IsolaattiTheme
|
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.components.PostAttachments
|
||||||
import com.isolaatti.posting.posts.domain.PostingSteps
|
import com.isolaatti.posting.posts.domain.PostingSteps
|
||||||
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.io.File
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class CreatePostActivity : IsolaattiBaseActivity() {
|
class CreatePostActivity : IsolaattiBaseActivity() {
|
||||||
@ -87,7 +98,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
|
|
||||||
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
||||||
if(it != null) {
|
if(it != null) {
|
||||||
viewModel.addPicture(it)
|
viewModel.addPicture(LocalImage(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,9 +111,37 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
|
|
||||||
private val takePhotoLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) {
|
private val takePhotoLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) {
|
||||||
if(it && cameraPhotoUri != null) {
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@ -110,6 +149,16 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
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 {
|
setContent {
|
||||||
val text by viewModel.liveContent.observeAsState("")
|
val text by viewModel.liveContent.observeAsState("")
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
@ -119,9 +168,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
mode == EXTRA_MODE_EDIT && postId != 0L
|
mode == EXTRA_MODE_EDIT && postId != 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
IsolaattiTheme {
|
IsolaattiTheme {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -158,6 +205,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
val posting by viewModel.sendingPost.observeAsState()
|
val posting by viewModel.sendingPost.observeAsState()
|
||||||
|
var audioRecorderIsVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if(posting == true) {
|
if(posting == true) {
|
||||||
val postingStep by viewModel.postingStep.collectAsState()
|
val postingStep by viewModel.postingStep.collectAsState()
|
||||||
@ -187,6 +235,9 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
Column(modifier = Modifier.padding(it).verticalScroll(scrollState)) {
|
Column(modifier = Modifier.padding(it).verticalScroll(scrollState)) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = { textFieldValue ->
|
onValueChange = { textFieldValue ->
|
||||||
@ -203,21 +254,93 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
unfocusedBorderColor = Color.Transparent
|
unfocusedBorderColor = Color.Transparent
|
||||||
),
|
),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(onClick = {}) {
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
audioRecorderIsVisible = !audioRecorderIsVisible
|
||||||
|
}
|
||||||
|
) {
|
||||||
Icon(painterResource(id = R.drawable.baseline_mic_24), null)
|
Icon(painterResource(id = R.drawable.baseline_mic_24), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(false) {
|
LaunchedEffect(Unit) {
|
||||||
AudioRecorder(
|
focusRequester.requestFocus()
|
||||||
onAudioRecorded = {
|
|
||||||
|
|
||||||
},
|
|
||||||
dismissible = true,
|
|
||||||
onDismiss = {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
intent.extras?.let {
|
||||||
mode = it.getInt(EXTRA_KEY_MODE, EXTRA_MODE_CREATE)
|
mode = it.getInt(EXTRA_KEY_MODE, EXTRA_MODE_CREATE)
|
||||||
postId = it.getLong(EXTRA_KEY_POST_ID)
|
postId = it.getLong(EXTRA_KEY_POST_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
|
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
|
||||||
viewModel.loadDiscussion(postId)
|
viewModel.loadDiscussion(postId)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun exit() {
|
private fun exit() {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
|
||||||
|
MediaController.releaseFuture(mediaControllerFuture)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -3,7 +3,7 @@ package com.isolaatti.profile.data.repository
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.isolaatti.auth.data.local.UserInfoDao
|
import com.isolaatti.auth.data.local.UserInfoDao
|
||||||
import com.isolaatti.auth.data.local.UserInfoEntity
|
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.ProfileApi
|
||||||
import com.isolaatti.profile.data.remote.UpdateProfileDto
|
import com.isolaatti.profile.data.remote.UpdateProfileDto
|
||||||
import com.isolaatti.profile.domain.ProfileRepository
|
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 {
|
try {
|
||||||
val result = profileApi.setProfileImage(image.id).awaitResponse()
|
val result = profileApi.setProfileImage(image.id).awaitResponse()
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package com.isolaatti.profile.domain
|
package com.isolaatti.profile.domain
|
||||||
|
|
||||||
import com.isolaatti.images.common.domain.entity.Image
|
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||||
import com.isolaatti.profile.data.remote.UpdateProfileDto
|
|
||||||
import com.isolaatti.profile.domain.entity.UserProfile
|
import com.isolaatti.profile.domain.entity.UserProfile
|
||||||
import com.isolaatti.utils.Resource
|
import com.isolaatti.utils.Resource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -9,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
interface ProfileRepository {
|
interface ProfileRepository {
|
||||||
fun getProfile(userId: Int): Flow<Resource<UserProfile>>
|
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>>
|
fun updateProfile(newDisplayName: String, newDescription: String): Flow<Resource<Boolean>>
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package com.isolaatti.profile.domain.use_case
|
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.profile.domain.ProfileRepository
|
||||||
import com.isolaatti.utils.Resource
|
import com.isolaatti.utils.Resource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SetProfileImage @Inject constructor(private val profileRepository: ProfileRepository) {
|
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.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.isolaatti.followers.domain.FollowingState
|
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.PostListingViewModelBase
|
||||||
import com.isolaatti.posting.posts.presentation.UpdateEvent
|
import com.isolaatti.posting.posts.presentation.UpdateEvent
|
||||||
import com.isolaatti.profile.domain.entity.UserProfile
|
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 {
|
viewModelScope.launch {
|
||||||
setProfileImageUC(image).onEach {
|
setProfileImageUC(image).onEach {
|
||||||
_profile.postValue(_profile.value?.copy(profileImageId = image.id))
|
_profile.postValue(_profile.value?.copy(profileImageId = image.id))
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
@ -23,7 +22,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import com.isolaatti.BuildConfig
|
import com.isolaatti.BuildConfig
|
||||||
import com.isolaatti.R
|
import com.isolaatti.R
|
||||||
import com.isolaatti.audio.common.domain.Audio
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
import com.isolaatti.audio.common.domain.Playable
|
|
||||||
import com.isolaatti.common.CoilImageLoader.imageLoader
|
import com.isolaatti.common.CoilImageLoader.imageLoader
|
||||||
import com.isolaatti.common.Dialogs
|
import com.isolaatti.common.Dialogs
|
||||||
import com.isolaatti.common.ErrorMessageViewModel
|
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.databinding.FragmentDiscussionsBinding
|
||||||
import com.isolaatti.followers.domain.FollowingState
|
import com.isolaatti.followers.domain.FollowingState
|
||||||
import com.isolaatti.hashtags.ui.HashtagsPostsActivity
|
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.images.picture_viewer.ui.PictureViewerActivity
|
||||||
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
|
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
|
||||||
import com.isolaatti.posting.posts.domain.entity.Post
|
import com.isolaatti.posting.posts.domain.entity.Post
|
||||||
@ -182,7 +180,7 @@ class ProfileMainFragment : Fragment() {
|
|||||||
val profilePictureUrl = profile?.profilePictureUrl
|
val profilePictureUrl = profile?.profilePictureUrl
|
||||||
if(profilePictureUrl != null) {
|
if(profilePictureUrl != null) {
|
||||||
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(
|
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(
|
||||||
Image(profile.profileImageId ?: "")
|
RemoteImage(profile.profileImageId ?: "")
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,20 @@
|
|||||||
package com.isolaatti.utils
|
package com.isolaatti.utils
|
||||||
|
|
||||||
fun Int.clockFormat(): String {
|
import java.lang.StringBuilder
|
||||||
val minutes = this / 60
|
import kotlin.time.Duration
|
||||||
val seconds = this % 60
|
|
||||||
|
|
||||||
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="invalid_arg">Invalid value passed</string>
|
||||||
<string name="posting">Posting</string>
|
<string name="posting">Posting</string>
|
||||||
<string name="uploading_photos">Uploading photos</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">
|
<string-array name="report_reasons">
|
||||||
<item>Spam</item>
|
<item>Spam</item>
|
||||||
<item>Explicit content</item>
|
<item>Explicit content</item>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user