mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +02:00
Merge branch 'master' into add-selection
# Conflicts: # app/src/main/java/app/myzel394/alibi/services/RecorderService.kt # app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt # app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt # app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt
This commit is contained in:
commit
2c58ab3c18
@ -3,10 +3,11 @@ package app.myzel394.alibi.db
|
||||
import android.media.MediaRecorder
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import app.myzel394.alibi.R
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter.ISO_DATE_TIME
|
||||
@ -14,6 +15,7 @@ import java.time.format.DateTimeFormatter.ISO_DATE_TIME
|
||||
@Serializable
|
||||
data class AppSettings(
|
||||
val audioRecorderSettings: AudioRecorderSettings = AudioRecorderSettings(),
|
||||
val notificationSettings: NotificationSettings? = null,
|
||||
val hasSeenOnboarding: Boolean = false,
|
||||
val showAdvancedSettings: Boolean = false,
|
||||
val theme: Theme = Theme.SYSTEM,
|
||||
@ -26,6 +28,10 @@ data class AppSettings(
|
||||
return copy(audioRecorderSettings = audioRecorderSettings)
|
||||
}
|
||||
|
||||
fun setNotificationSettings(notificationSettings: NotificationSettings?): AppSettings {
|
||||
return copy(notificationSettings = notificationSettings)
|
||||
}
|
||||
|
||||
fun setHasSeenOnboarding(hasSeenOnboarding: Boolean): AppSettings {
|
||||
return copy(hasSeenOnboarding = hasSeenOnboarding)
|
||||
}
|
||||
@ -40,45 +46,18 @@ data class AppSettings(
|
||||
DARK,
|
||||
}
|
||||
|
||||
fun toJSONObject(): JSONObject {
|
||||
return JSONObject(
|
||||
mapOf(
|
||||
"audioRecorderSettings" to audioRecorderSettings.toJSONObject(),
|
||||
"hasSeenOnboarding" to hasSeenOnboarding,
|
||||
"showAdvancedSettings" to showAdvancedSettings,
|
||||
"theme" to theme.name,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun exportToString(): String {
|
||||
return JSONObject(
|
||||
mapOf(
|
||||
"_meta" to mapOf(
|
||||
"version" to 1,
|
||||
"date" to LocalDateTime.now().format(ISO_DATE_TIME),
|
||||
"app" to "app.myzel394.alibi",
|
||||
),
|
||||
"data" to toJSONObject(),
|
||||
)
|
||||
).toString(0)
|
||||
return Json.encodeToString(serializer(), this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getDefaultInstance(): AppSettings = AppSettings()
|
||||
|
||||
fun fromJSONObject(data: JSONObject): AppSettings {
|
||||
return AppSettings(
|
||||
audioRecorderSettings = AudioRecorderSettings.fromJSONObject(data.getJSONObject("audioRecorderSettings")),
|
||||
hasSeenOnboarding = data.getBoolean("hasSeenOnboarding"),
|
||||
showAdvancedSettings = data.getBoolean("showAdvancedSettings"),
|
||||
theme = Theme.valueOf(data.getString("theme")),
|
||||
)
|
||||
}
|
||||
|
||||
fun fromExportedString(data: String): AppSettings {
|
||||
val json = JSONObject(data)
|
||||
return fromJSONObject(json.getJSONObject("data"))
|
||||
return Json.decodeFromString(
|
||||
serializer(),
|
||||
data,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -334,20 +313,6 @@ data class AudioRecorderSettings(
|
||||
return supportedFormats.contains(outputFormat)
|
||||
}
|
||||
|
||||
fun toJSONObject(): JSONObject {
|
||||
return JSONObject(
|
||||
mapOf(
|
||||
"maxDuration" to maxDuration,
|
||||
"intervalDuration" to intervalDuration,
|
||||
"forceExactMaxDuration" to forceExactMaxDuration,
|
||||
"bitRate" to bitRate,
|
||||
"samplingRate" to samplingRate,
|
||||
"outputFormat" to outputFormat,
|
||||
"encoder" to encoder,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getDefaultInstance(): AudioRecorderSettings = AudioRecorderSettings()
|
||||
val EXAMPLE_MAX_DURATIONS = listOf(
|
||||
@ -454,23 +419,82 @@ data class AudioRecorderSettings(
|
||||
}
|
||||
}
|
||||
}).toMap()
|
||||
|
||||
fun fromJSONObject(data: JSONObject): AudioRecorderSettings {
|
||||
return AudioRecorderSettings(
|
||||
maxDuration = data.getLong("maxDuration"),
|
||||
intervalDuration = data.getLong("intervalDuration"),
|
||||
forceExactMaxDuration = data.getBoolean("forceExactMaxDuration"),
|
||||
bitRate = data.getInt("bitRate"),
|
||||
samplingRate = data.optInt("samplingRate", -1).let {
|
||||
if (it == -1) null else it
|
||||
},
|
||||
outputFormat = data.optInt("outputFormat", -1).let {
|
||||
if (it == -1) null else it
|
||||
},
|
||||
encoder = data.optInt("encoder", -1).let {
|
||||
if (it == -1) null else it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NotificationSettings(
|
||||
val title: String,
|
||||
val message: String,
|
||||
val iconID: Int,
|
||||
val showOngoing: Boolean,
|
||||
val preset: Preset? = null,
|
||||
) {
|
||||
@Serializable
|
||||
sealed class Preset(
|
||||
val titleID: Int,
|
||||
val messageID: Int,
|
||||
val showOngoing: Boolean,
|
||||
val iconID: Int,
|
||||
) {
|
||||
@Serializable
|
||||
data object Default : Preset(
|
||||
R.string.ui_audioRecorder_state_recording_title,
|
||||
R.string.ui_audioRecorder_state_recording_description,
|
||||
true,
|
||||
R.drawable.launcher_monochrome_noopacity,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data object Weather : Preset(
|
||||
R.string.ui_audioRecorder_state_recording_fake_weather_title,
|
||||
R.string.ui_audioRecorder_state_recording_fake_weather_description,
|
||||
false,
|
||||
R.drawable.ic_cloud
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data object Player : Preset(
|
||||
R.string.ui_audioRecorder_state_recording_fake_player_title,
|
||||
R.string.ui_audioRecorder_state_recording_fake_player_description,
|
||||
true,
|
||||
R.drawable.ic_note,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data object Browser : Preset(
|
||||
R.string.ui_audioRecorder_state_recording_fake_browser_title,
|
||||
R.string.ui_audioRecorder_state_recording_fake_browser_description,
|
||||
true,
|
||||
R.drawable.ic_download,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data object VPN : Preset(
|
||||
R.string.ui_audioRecorder_state_recording_fake_vpn_title,
|
||||
R.string.ui_audioRecorder_state_recording_fake_vpn_description,
|
||||
false,
|
||||
R.drawable.ic_vpn,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromPreset(preset: Preset): NotificationSettings {
|
||||
return NotificationSettings(
|
||||
title = "",
|
||||
message = "",
|
||||
showOngoing = preset.showOngoing,
|
||||
iconID = preset.iconID,
|
||||
preset = preset,
|
||||
)
|
||||
}
|
||||
|
||||
val PRESETS = listOf(
|
||||
Preset.Default,
|
||||
Preset.Weather,
|
||||
Preset.Player,
|
||||
Preset.Browser,
|
||||
Preset.VPN,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class AppSettingsSerializer: Serializer<AppSettings> {
|
||||
class AppSettingsSerializer : Serializer<AppSettings> {
|
||||
override val defaultValue: AppSettings = AppSettings.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): AppSettings {
|
||||
@ -39,8 +39,9 @@ class AppSettingsSerializer: Serializer<AppSettings> {
|
||||
}
|
||||
}
|
||||
|
||||
class LocalDateTimeSerializer: KSerializer<LocalDateTime> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime {
|
||||
return LocalDateTime.parse(decoder.decodeString())
|
||||
|
@ -0,0 +1,147 @@
|
||||
package app.myzel394.alibi.services
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import app.myzel394.alibi.MainActivity
|
||||
import app.myzel394.alibi.NotificationHelper
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.db.NotificationSettings
|
||||
import app.myzel394.alibi.enums.RecorderState
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
data class RecorderNotificationHelper(
|
||||
val context: Context,
|
||||
val details: NotificationDetails? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class NotificationDetails(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val icon: Int,
|
||||
val isOngoing: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
fun fromNotificationSettings(
|
||||
context: Context,
|
||||
settings: NotificationSettings,
|
||||
): NotificationDetails {
|
||||
return if (settings.preset == null) {
|
||||
NotificationDetails(
|
||||
settings.title,
|
||||
settings.message,
|
||||
settings.iconID,
|
||||
settings.showOngoing,
|
||||
)
|
||||
} else {
|
||||
NotificationDetails(
|
||||
context.getString(settings.preset.titleID),
|
||||
context.getString(settings.preset.messageID),
|
||||
settings.preset.iconID,
|
||||
settings.preset.showOngoing,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNotificationChangeStateIntent(
|
||||
newState: RecorderState,
|
||||
requestCode: Int
|
||||
): PendingIntent {
|
||||
return PendingIntent.getService(
|
||||
context,
|
||||
requestCode,
|
||||
Intent(context, AudioRecorderService::class.java).apply {
|
||||
action = "changeState"
|
||||
putExtra("newState", newState.name)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconID(): Int = details?.icon ?: R.drawable.launcher_monochrome_noopacity
|
||||
|
||||
private fun createBaseNotification(): NotificationCompat.Builder {
|
||||
return NotificationCompat.Builder(
|
||||
context,
|
||||
NotificationHelper.RECORDER_CHANNEL_ID
|
||||
)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setSmallIcon(getIconID())
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.setSilent(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setChronometerCountDown(false)
|
||||
}
|
||||
|
||||
fun buildStartingNotification(): Notification {
|
||||
return createBaseNotification()
|
||||
.setContentTitle(context.getString(R.string.ui_audioRecorder_state_recording_title))
|
||||
.setContentText(context.getString(R.string.ui_audioRecorder_state_recording_description))
|
||||
.build()
|
||||
}
|
||||
|
||||
fun buildRecordingNotification(recordingTime: Long): Notification {
|
||||
return createBaseNotification()
|
||||
.setUsesChronometer(details?.isOngoing ?: true)
|
||||
.setOngoing(details?.isOngoing ?: true)
|
||||
.setShowWhen(details?.isOngoing ?: true)
|
||||
.setWhen(
|
||||
Date.from(
|
||||
Calendar
|
||||
.getInstance()
|
||||
.also { it.add(Calendar.MILLISECOND, -recordingTime.toInt()) }
|
||||
.toInstant()
|
||||
).time,
|
||||
)
|
||||
.addAction(
|
||||
R.drawable.ic_cancel,
|
||||
context.getString(R.string.ui_audioRecorder_action_delete_label),
|
||||
getNotificationChangeStateIntent(RecorderState.IDLE, 1),
|
||||
)
|
||||
.addAction(
|
||||
R.drawable.ic_pause,
|
||||
context.getString(R.string.ui_audioRecorder_action_pause_label),
|
||||
getNotificationChangeStateIntent(RecorderState.PAUSED, 2),
|
||||
)
|
||||
.setContentTitle(
|
||||
details?.title
|
||||
?: context.getString(R.string.ui_audioRecorder_state_recording_title)
|
||||
)
|
||||
.setContentText(
|
||||
details?.description
|
||||
?: context.getString(R.string.ui_audioRecorder_state_recording_description)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun buildPausedNotification(start: LocalDateTime): Notification {
|
||||
return createBaseNotification()
|
||||
.setContentTitle(context.getString(R.string.ui_audioRecorder_state_paused_title))
|
||||
.setContentText(context.getString(R.string.ui_audioRecorder_state_paused_description))
|
||||
.setOngoing(false)
|
||||
.setUsesChronometer(false)
|
||||
.setWhen(Date.from(start.atZone(ZoneId.systemDefault()).toInstant()).time)
|
||||
.addAction(
|
||||
R.drawable.ic_play,
|
||||
context.getString(R.string.ui_audioRecorder_action_resume_label),
|
||||
getNotificationChangeStateIntent(RecorderState.RECORDING, 3),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
@ -2,22 +2,19 @@ package app.myzel394.alibi.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import app.myzel394.alibi.MainActivity
|
||||
import androidx.core.app.ServiceCompat
|
||||
import app.myzel394.alibi.NotificationHelper
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.enums.RecorderState
|
||||
import app.myzel394.alibi.ui.utils.PermissionHelper
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -40,6 +37,7 @@ abstract class RecorderService : Service() {
|
||||
private set
|
||||
private lateinit var recordingTimeTimer: ScheduledExecutorService
|
||||
var onRecordingTimeChange: ((Long) -> Unit)? = null
|
||||
var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null
|
||||
|
||||
protected abstract fun start()
|
||||
protected abstract fun pause()
|
||||
@ -50,6 +48,15 @@ abstract class RecorderService : Service() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
"init" -> {
|
||||
notificationDetails = intent.getStringExtra("notificationDetails")?.let {
|
||||
Json.decodeFromString(
|
||||
RecorderNotificationHelper.NotificationDetails.serializer(),
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"changeState" -> {
|
||||
val newState = intent.getStringExtra("newState")?.let {
|
||||
RecorderState.valueOf(it)
|
||||
@ -135,8 +142,16 @@ abstract class RecorderService : Service() {
|
||||
fun startRecording() {
|
||||
recordingStart = LocalDateTime.now()
|
||||
|
||||
val notification = buildStartNotification()
|
||||
startForeground(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID, notification)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID,
|
||||
getNotificationHelper().buildStartingNotification(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
|
||||
// Start
|
||||
changeState(RecorderState.RECORDING)
|
||||
@ -154,102 +169,26 @@ abstract class RecorderService : Service() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun buildStartNotification(): Notification =
|
||||
NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.ui_audioRecorder_state_recording_title))
|
||||
.setContentText(getString(R.string.ui_audioRecorder_state_recording_description))
|
||||
.setSmallIcon(R.drawable.launcher_foreground)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.build()
|
||||
|
||||
private fun getNotificationChangeStateIntent(
|
||||
newState: RecorderState,
|
||||
requestCode: Int
|
||||
): PendingIntent {
|
||||
return PendingIntent.getService(
|
||||
this,
|
||||
requestCode,
|
||||
Intent(this, AudioRecorderService::class.java).apply {
|
||||
action = "changeState"
|
||||
putExtra("newState", newState.name)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
private fun getNotificationHelper(): RecorderNotificationHelper {
|
||||
return RecorderNotificationHelper(this, notificationDetails)
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification = when (state) {
|
||||
RecorderState.RECORDING -> NotificationCompat.Builder(
|
||||
this,
|
||||
NotificationHelper.RECORDER_CHANNEL_ID
|
||||
)
|
||||
.setContentTitle(getString(R.string.ui_audioRecorder_state_recording_title))
|
||||
.setContentText(getString(R.string.ui_audioRecorder_state_recording_description))
|
||||
.setSmallIcon(R.drawable.launcher_foreground)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setOngoing(true)
|
||||
.setWhen(
|
||||
Date.from(
|
||||
Calendar
|
||||
.getInstance()
|
||||
.also { it.add(Calendar.MILLISECOND, -recordingTime.toInt()) }
|
||||
.toInstant()
|
||||
).time,
|
||||
)
|
||||
.setSilent(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setUsesChronometer(true)
|
||||
.setChronometerCountDown(false)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.addAction(
|
||||
R.drawable.ic_cancel,
|
||||
getString(R.string.ui_audioRecorder_action_delete_label),
|
||||
getNotificationChangeStateIntent(RecorderState.IDLE, 1),
|
||||
)
|
||||
.addAction(
|
||||
R.drawable.ic_pause,
|
||||
getString(R.string.ui_audioRecorder_action_pause_label),
|
||||
getNotificationChangeStateIntent(RecorderState.PAUSED, 2),
|
||||
)
|
||||
.build()
|
||||
|
||||
RecorderState.PAUSED -> NotificationCompat.Builder(
|
||||
this,
|
||||
NotificationHelper.RECORDER_CHANNEL_ID
|
||||
)
|
||||
.setContentTitle(getString(R.string.ui_audioRecorder_state_paused_title))
|
||||
.setContentText(getString(R.string.ui_audioRecorder_state_paused_description))
|
||||
.setSmallIcon(R.drawable.launcher_foreground)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setOngoing(false)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setUsesChronometer(false)
|
||||
.setWhen(Date.from(recordingStart.atZone(ZoneId.systemDefault()).toInstant()).time)
|
||||
.setShowWhen(true)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, MainActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.addAction(
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.ui_audioRecorder_action_resume_label),
|
||||
getNotificationChangeStateIntent(RecorderState.RECORDING, 3),
|
||||
)
|
||||
.build()
|
||||
private fun buildNotification(): Notification {
|
||||
val notificationHelper = getNotificationHelper()
|
||||
|
||||
else -> throw IllegalStateException("Invalid state passed to `buildNotification()`")
|
||||
return when (state) {
|
||||
RecorderState.RECORDING -> {
|
||||
notificationHelper.buildRecordingNotification(recordingTime)
|
||||
}
|
||||
|
||||
RecorderState.PAUSED -> {
|
||||
notificationHelper.buildPausedNotification(recordingStart)
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw IllegalStateException("Notification can't be built in state $state")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package app.myzel394.alibi.ui
|
||||
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -25,6 +28,7 @@ import app.myzel394.alibi.db.LastRecording
|
||||
import app.myzel394.alibi.ui.enums.Screen
|
||||
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||
import app.myzel394.alibi.ui.screens.AudioRecorder
|
||||
import app.myzel394.alibi.ui.screens.CustomRecordingNotificationsScreen
|
||||
import app.myzel394.alibi.ui.screens.SettingsScreen
|
||||
import app.myzel394.alibi.ui.screens.WelcomeScreen
|
||||
|
||||
@ -90,5 +94,22 @@ fun Navigation(
|
||||
audioRecorder = audioRecorder,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
Screen.CustomRecordingNotifications.route,
|
||||
enterTransition = {
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { it -> it / 2 }
|
||||
) + fadeIn()
|
||||
},
|
||||
exitTransition = {
|
||||
slideOutHorizontally(
|
||||
targetOffsetX = { it -> it / 2 }
|
||||
) + fadeOut(tween(150))
|
||||
}
|
||||
) {
|
||||
CustomRecordingNotificationsScreen(
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,8 +20,15 @@ import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@ -31,22 +38,51 @@ import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.alibi.NotificationHelper
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.dataStore
|
||||
import app.myzel394.alibi.db.AppSettings
|
||||
import app.myzel394.alibi.services.RecorderNotificationHelper
|
||||
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
|
||||
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
|
||||
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.lastOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
|
||||
@Composable
|
||||
fun StartRecording(
|
||||
audioRecorder: AudioRecorderModel,
|
||||
// Loading this from parent, because if we load it ourselves
|
||||
// and permissions have already been granted, initial
|
||||
// settings will be used, instead of the actual settings.
|
||||
appSettings: AppSettings
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// We can't get the current `notificationDetails` inside the
|
||||
// `onPermissionAvailable` function. We'll instead use this hack
|
||||
// with `LaunchedEffect` to get the current value.
|
||||
var startRecording by rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(startRecording) {
|
||||
if (startRecording) {
|
||||
startRecording = false
|
||||
audioRecorder.notificationDetails = appSettings.notificationSettings.let {
|
||||
if (it == null)
|
||||
null
|
||||
else
|
||||
RecorderNotificationHelper.NotificationDetails.fromNotificationSettings(
|
||||
context,
|
||||
it
|
||||
)
|
||||
}
|
||||
audioRecorder.startRecording(context)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
@ -57,14 +93,12 @@ fun StartRecording(
|
||||
permission = Manifest.permission.RECORD_AUDIO,
|
||||
icon = Icons.Default.Mic,
|
||||
onPermissionAvailable = {
|
||||
audioRecorder.startRecording(context)
|
||||
startRecording = true
|
||||
},
|
||||
) { trigger ->
|
||||
val label = stringResource(R.string.ui_audioRecorder_action_start_label)
|
||||
Button(
|
||||
onClick = {
|
||||
trigger()
|
||||
},
|
||||
onClick = trigger,
|
||||
modifier = Modifier
|
||||
.semantics {
|
||||
contentDescription = label
|
||||
@ -98,7 +132,10 @@ fun StartRecording(
|
||||
.value
|
||||
|
||||
Text(
|
||||
stringResource(R.string.ui_audioRecorder_action_start_description, settings.audioRecorderSettings.maxDuration / 1000 / 60),
|
||||
stringResource(
|
||||
R.string.ui_audioRecorder_action_start_description,
|
||||
settings.audioRecorderSettings.maxDuration / 1000 / 60
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@ -115,7 +152,8 @@ fun StartRecording(
|
||||
) {
|
||||
val label = stringResource(
|
||||
R.string.ui_audioRecorder_action_saveOldRecording_label,
|
||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(audioRecorder.lastRecording!!.recordingStart),
|
||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
|
||||
.format(audioRecorder.lastRecording!!.recordingStart),
|
||||
)
|
||||
Button(
|
||||
modifier = Modifier
|
||||
@ -140,8 +178,7 @@ fun StartRecording(
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
} else
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowRightAlt
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.ui.utils.openNotificationsSettings
|
||||
|
||||
@Composable
|
||||
fun LandingElement(
|
||||
modifier: Modifier = Modifier,
|
||||
onOpenEditor: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 32.dp, vertical = 64.dp)
|
||||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Box() {}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_custom_recording_notifications_blob),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.tertiaryContainer),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.width(512.dp)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier
|
||||
.size(128.dp)
|
||||
)
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.ui_settings_customNotifications_landing_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.ui_settings_customNotifications_landing_description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Button(
|
||||
onClick = onOpenEditor,
|
||||
colors = ButtonDefaults.filledTonalButtonColors(),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(ButtonDefaults.IconSize)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.ui_settings_customNotifications_landing_getStarted
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = context::openNotificationsSettings,
|
||||
colors = ButtonDefaults.textButtonColors(),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.ui_settings_customNotifications_landing_help_hideNotifications),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.alibi.db.NotificationSettings
|
||||
|
||||
@Composable
|
||||
fun NotificationPresetSelect(
|
||||
modifier: Modifier = Modifier,
|
||||
preset: NotificationSettings.Preset
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.then(modifier)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f)
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
PreviewIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
painter = painterResource(id = preset.iconID),
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(preset.titleID),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(preset.messageID),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Normal,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.alibi.R
|
||||
|
||||
@Composable
|
||||
fun PreviewIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
painter: Painter,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.then(modifier)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.secondary)
|
||||
.padding(2.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.models
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.db.NotificationSettings
|
||||
|
||||
class NotificationViewModel : ViewModel() {
|
||||
// We want to show the actual translated strings of the preset
|
||||
// in the preview but don't want to save them to the database
|
||||
// because they should be retrieved in the notification itself.
|
||||
// Thus we save whether the preset has been changed by the user
|
||||
private var _presetChanged = false
|
||||
|
||||
private var _title = mutableStateOf("")
|
||||
val title: String
|
||||
get() = _title.value
|
||||
private var _description = mutableStateOf("")
|
||||
val description: String
|
||||
get() = _description.value
|
||||
|
||||
var showOngoing: Boolean by mutableStateOf(true)
|
||||
var icon: Int by mutableIntStateOf(R.drawable.launcher_monochrome_noopacity)
|
||||
|
||||
// `preset` can't be used as a variable name here because
|
||||
// the compiler throws a strange error then
|
||||
var notificationPreset: NotificationSettings.Preset? by mutableStateOf(null)
|
||||
|
||||
private var _hasBeenInitialized = false;
|
||||
|
||||
|
||||
fun setPreset(title: String, description: String, preset: NotificationSettings.Preset) {
|
||||
_presetChanged = false
|
||||
|
||||
_title.value = title
|
||||
_description.value = description
|
||||
showOngoing = preset.showOngoing
|
||||
icon = preset.iconID
|
||||
this.notificationPreset = preset
|
||||
}
|
||||
|
||||
fun setTitle(title: String) {
|
||||
_presetChanged = true
|
||||
_title.value = title
|
||||
}
|
||||
|
||||
fun setDescription(description: String) {
|
||||
_presetChanged = true
|
||||
_description.value = description
|
||||
}
|
||||
|
||||
fun initialize(
|
||||
title: String,
|
||||
description: String,
|
||||
showOngoing: Boolean = true,
|
||||
icon: Int = R.drawable.launcher_monochrome_noopacity,
|
||||
) {
|
||||
_title.value = title
|
||||
_description.value = description
|
||||
this.showOngoing = showOngoing
|
||||
this.icon = icon
|
||||
_hasBeenInitialized = true
|
||||
}
|
||||
|
||||
fun asNotificationSettings(): NotificationSettings {
|
||||
return if (!_presetChanged && notificationPreset != null) {
|
||||
NotificationSettings.fromPreset(notificationPreset!!)
|
||||
} else {
|
||||
NotificationSettings(
|
||||
title = title,
|
||||
message = description,
|
||||
iconID = icon,
|
||||
showOngoing = showOngoing,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.PreviewIcon
|
||||
import app.myzel394.alibi.ui.effects.rememberForceUpdate
|
||||
import com.maxkeppeler.sheets.input.models.InputText
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import java.time.Period
|
||||
|
||||
@Composable
|
||||
fun EditNotificationInput(
|
||||
modifier: Modifier = Modifier,
|
||||
showOngoing: Boolean,
|
||||
title: String,
|
||||
description: String,
|
||||
icon: Painter,
|
||||
onShowOngoingChange: (Boolean) -> Unit,
|
||||
onTitleChange: (String) -> Unit,
|
||||
onDescriptionChange: (String) -> Unit,
|
||||
onIconChange: (Int) -> Unit,
|
||||
) {
|
||||
var ongoingStartTime by remember { mutableStateOf(LocalDateTime.now()) }
|
||||
|
||||
val secondaryColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
|
||||
LaunchedEffect(showOngoing) {
|
||||
if (showOngoing) {
|
||||
ongoingStartTime = LocalDateTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f))
|
||||
.padding(16.dp)
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
val headlineSize = 22.dp
|
||||
|
||||
PreviewIcon(
|
||||
modifier = Modifier.size(headlineSize),
|
||||
painter = icon,
|
||||
)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.height(headlineSize),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = secondaryColor,
|
||||
)
|
||||
if (showOngoing) {
|
||||
Icon(
|
||||
Icons.Default.Circle,
|
||||
contentDescription = null,
|
||||
tint = secondaryColor,
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
)
|
||||
|
||||
val fakeAlpha = rememberForceUpdate()
|
||||
val formattedTime = {
|
||||
val difference =
|
||||
Duration.between(
|
||||
ongoingStartTime,
|
||||
LocalDateTime.now(),
|
||||
)
|
||||
val minutes = difference.toMinutes()
|
||||
val seconds = difference.minusMinutes(minutes).seconds
|
||||
|
||||
"${if (minutes < 10) "0$minutes" else minutes}:${if (seconds < 10) "0$seconds" else seconds}"
|
||||
}
|
||||
Text(
|
||||
formattedTime(),
|
||||
modifier = Modifier.alpha(fakeAlpha),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = secondaryColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
BasicTextField(
|
||||
value = title,
|
||||
onValueChange = onTitleChange,
|
||||
textStyle = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
)
|
||||
BasicTextField(
|
||||
value = description,
|
||||
onValueChange = onDescriptionChange,
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.ui_audioRecorder_action_delete_label),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.ui_audioRecorder_action_pause_label),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.db.NotificationSettings
|
||||
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NotificationPresetsRoulette(
|
||||
onClick: (String, String, NotificationSettings.Preset) -> Unit,
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
state = state,
|
||||
flingBehavior = rememberSnapFlingBehavior(lazyListState = state)
|
||||
) {
|
||||
items(NotificationSettings.PRESETS.size) {
|
||||
val preset = NotificationSettings.PRESETS[it]
|
||||
|
||||
val label = stringResource(
|
||||
R.string.ui_settings_customNotifications_preset_apply_label,
|
||||
stringResource(preset.titleID)
|
||||
)
|
||||
val presetTitle = stringResource(preset.titleID)
|
||||
val presetDescription = stringResource(preset.messageID)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.width(
|
||||
LocalConfiguration.current.screenWidthDp.dp,
|
||||
)
|
||||
) {
|
||||
NotificationPresetSelect(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(.95f)
|
||||
.align(Alignment.Center)
|
||||
.semantics {
|
||||
contentDescription = label
|
||||
}
|
||||
.clickable {
|
||||
onClick(
|
||||
presetTitle,
|
||||
presetDescription,
|
||||
preset,
|
||||
)
|
||||
},
|
||||
preset = preset,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.dataStore
|
||||
import app.myzel394.alibi.db.AppSettings
|
||||
import app.myzel394.alibi.db.NotificationSettings
|
||||
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.models.NotificationViewModel
|
||||
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput
|
||||
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.NotificationPresetsRoulette
|
||||
import app.myzel394.alibi.ui.components.atoms.MessageBox
|
||||
import app.myzel394.alibi.ui.components.atoms.MessageType
|
||||
|
||||
val HORIZONTAL_PADDING = 16.dp;
|
||||
|
||||
@Composable
|
||||
fun NotificationEditor(
|
||||
modifier: Modifier = Modifier,
|
||||
notificationModel: NotificationViewModel = viewModel(),
|
||||
onNotificationChange: (NotificationSettings) -> Unit,
|
||||
) {
|
||||
val dataStore = LocalContext.current.dataStore
|
||||
val settings = dataStore
|
||||
.data
|
||||
.collectAsState(initial = AppSettings.getDefaultInstance())
|
||||
.value
|
||||
|
||||
if (settings.notificationSettings != null) {
|
||||
val title = settings.notificationSettings.let {
|
||||
if (it.preset != null)
|
||||
stringResource(it.preset.titleID)
|
||||
else
|
||||
it.title
|
||||
}
|
||||
val description = settings.notificationSettings.let {
|
||||
if (it.preset != null)
|
||||
stringResource(it.preset.messageID)
|
||||
else
|
||||
it.message
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
notificationModel.initialize(
|
||||
title,
|
||||
description,
|
||||
settings.notificationSettings.showOngoing,
|
||||
settings.notificationSettings.iconID,
|
||||
)
|
||||
|
||||
if (settings.notificationSettings.preset != null) {
|
||||
notificationModel.setPreset(
|
||||
title,
|
||||
description,
|
||||
settings.notificationSettings.preset
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val defaultTitle = stringResource(R.string.ui_audioRecorder_state_recording_title)
|
||||
val defaultDescription =
|
||||
stringResource(R.string.ui_audioRecorder_state_recording_description)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
notificationModel.initialize(
|
||||
defaultTitle,
|
||||
defaultDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(modifier),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = HORIZONTAL_PADDING),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
MessageBox(
|
||||
type = MessageType.SURFACE,
|
||||
message = stringResource(R.string.ui_settings_customNotifications_edit_help)
|
||||
)
|
||||
|
||||
EditNotificationInput(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
showOngoing = notificationModel.showOngoing,
|
||||
title = notificationModel.title,
|
||||
description = notificationModel.description,
|
||||
icon = painterResource(notificationModel.icon),
|
||||
onShowOngoingChange = {
|
||||
notificationModel.showOngoing = it
|
||||
},
|
||||
onTitleChange = notificationModel::setTitle,
|
||||
onDescriptionChange = notificationModel::setDescription,
|
||||
onIconChange = {
|
||||
notificationModel.icon = it
|
||||
},
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.clickable {
|
||||
notificationModel.showOngoing = notificationModel.showOngoing.not()
|
||||
}
|
||||
.background(MaterialTheme.colorScheme.tertiaryContainer)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Checkbox(
|
||||
checked = notificationModel.showOngoing,
|
||||
onCheckedChange = {
|
||||
notificationModel.showOngoing = it
|
||||
},
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.tertiary,
|
||||
checkmarkColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.ui_settings_customNotifications_showOngoing_label),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||
) {
|
||||
NotificationPresetsRoulette(
|
||||
onClick = notificationModel::setPreset,
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onNotificationChange(
|
||||
notificationModel.asNotificationSettings()
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = HORIZONTAL_PADDING)
|
||||
.height(48.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(ButtonDefaults.IconSize)
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(ButtonDefaults.IconSpacing)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.ui_settings_customNotifications_save_label)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package app.myzel394.alibi.ui.components.SettingsScreen.atoms
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.navigation.NavController
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.dataStore
|
||||
import app.myzel394.alibi.db.AppSettings
|
||||
import app.myzel394.alibi.ui.components.atoms.SettingsTile
|
||||
import app.myzel394.alibi.ui.enums.Screen
|
||||
|
||||
@Composable
|
||||
fun CustomNotificationTile(
|
||||
navController: NavController,
|
||||
) {
|
||||
val dataStore = LocalContext.current.dataStore
|
||||
val settings = dataStore
|
||||
.data
|
||||
.collectAsState(initial = AppSettings.getDefaultInstance())
|
||||
.value
|
||||
|
||||
val label = if (settings.notificationSettings == null)
|
||||
stringResource(R.string.ui_settings_option_customNotification_description_setup)
|
||||
else stringResource(
|
||||
R.string.ui_settings_option_customNotification_description_edit
|
||||
)
|
||||
|
||||
SettingsTile(
|
||||
firstModifier = Modifier
|
||||
.clickable {
|
||||
navController.navigate(Screen.CustomRecordingNotifications.route)
|
||||
}
|
||||
.semantics { contentDescription = label },
|
||||
title = stringResource(R.string.ui_settings_option_customNotification_title),
|
||||
description = label,
|
||||
leading = {
|
||||
Icon(
|
||||
Icons.Default.Notifications,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
Icon(
|
||||
Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
@ -34,15 +34,17 @@ fun MessageBox(
|
||||
MessageType.INFO -> MaterialTheme.colorScheme.tertiaryContainer
|
||||
MessageType.SUCCESS -> Color.Green.copy(alpha = 0.3f)
|
||||
MessageType.WARNING -> Color.Yellow.copy(alpha = 0.3f)
|
||||
MessageType.SURFACE -> MaterialTheme.colorScheme.surfaceVariant
|
||||
}
|
||||
val onContainerColor = when (type) {
|
||||
MessageType.ERROR -> MaterialTheme.colorScheme.onErrorContainer
|
||||
MessageType.ERROR -> MaterialTheme.colorScheme.onError
|
||||
MessageType.INFO -> MaterialTheme.colorScheme.onTertiaryContainer
|
||||
MessageType.SUCCESS -> Color.Green
|
||||
MessageType.WARNING -> Color.Yellow
|
||||
MessageType.SURFACE -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
val textColor = if (isDark) onContainerColor else MaterialTheme.colorScheme.onSurface
|
||||
val backgroundColor = if (isDark) containerColor else containerColor
|
||||
val backgroundColor = if (isDark) containerColor else onContainerColor
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -56,6 +58,7 @@ fun MessageBox(
|
||||
imageVector = when (type) {
|
||||
MessageType.ERROR -> Icons.Default.Error
|
||||
MessageType.INFO -> Icons.Default.Info
|
||||
MessageType.SURFACE -> Icons.Default.Info
|
||||
MessageType.SUCCESS -> Icons.Default.Check
|
||||
MessageType.WARNING -> Icons.Default.Warning
|
||||
},
|
||||
@ -84,6 +87,7 @@ fun MessageBox(
|
||||
enum class MessageType {
|
||||
ERROR,
|
||||
INFO,
|
||||
SURFACE,
|
||||
SUCCESS,
|
||||
WARNING,
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package app.myzel394.alibi.ui.effects
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun rememberForceUpdate(
|
||||
time: Long = 100L,
|
||||
): Float {
|
||||
var tickTack by rememberSaveable { mutableStateOf(1f) }
|
||||
|
||||
LaunchedEffect(tickTack) {
|
||||
delay(time)
|
||||
tickTack = if (tickTack == 1f) 0.99f else 1f
|
||||
}
|
||||
|
||||
return tickTack
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
package app.myzel394.alibi.ui.enums
|
||||
|
||||
sealed class Screen(val route: String) {
|
||||
object AudioRecorder : Screen("audio-recorder")
|
||||
object Settings : Screen("settings")
|
||||
object Welcome : Screen("welcome")
|
||||
data object AudioRecorder : Screen("audio-recorder")
|
||||
data object Settings : Screen("settings")
|
||||
data object Welcome : Screen("welcome")
|
||||
data object CustomRecordingNotifications : Screen("custom-recording-notifications")
|
||||
|
||||
fun withArgs(vararg args: String): String {
|
||||
return buildString {
|
||||
|
@ -10,10 +10,14 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.myzel394.alibi.dataStore
|
||||
import app.myzel394.alibi.db.LastRecording
|
||||
import app.myzel394.alibi.enums.RecorderState
|
||||
import app.myzel394.alibi.services.AudioRecorderService
|
||||
import app.myzel394.alibi.services.RecorderNotificationHelper
|
||||
import app.myzel394.alibi.services.RecorderService
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.serialization.json.Json
|
||||
import app.myzel394.alibi.ui.utils.MicrophoneInfo
|
||||
|
||||
class AudioRecorderModel : ViewModel() {
|
||||
@ -45,6 +49,7 @@ class AudioRecorderModel : ViewModel() {
|
||||
|
||||
var onRecordingSave: () -> Unit = {}
|
||||
var onError: () -> Unit = {}
|
||||
var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null
|
||||
|
||||
var microphoneStatus: MicrophoneConnectivityStatus = MicrophoneConnectivityStatus.CONNECTED
|
||||
private set
|
||||
@ -112,7 +117,19 @@ class AudioRecorderModel : ViewModel() {
|
||||
context.unbindService(connection)
|
||||
}
|
||||
|
||||
val intent = Intent(context, AudioRecorderService::class.java)
|
||||
val intent = Intent(context, AudioRecorderService::class.java).apply {
|
||||
action = "init"
|
||||
|
||||
if (notificationDetails != null) {
|
||||
putExtra(
|
||||
"notificationDetails",
|
||||
Json.encodeToString(
|
||||
RecorderNotificationHelper.NotificationDetails.serializer(),
|
||||
notificationDetails!!,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
@ -34,6 +34,10 @@ import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording
|
||||
import app.myzel394.alibi.ui.enums.Screen
|
||||
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.dataStore
|
||||
import app.myzel394.alibi.db.AppSettings
|
||||
import app.myzel394.alibi.db.LastRecording
|
||||
import app.myzel394.alibi.services.RecorderNotificationHelper
|
||||
import app.myzel394.alibi.ui.effects.rememberSettings
|
||||
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||
import kotlinx.coroutines.launch
|
||||
@ -167,10 +171,13 @@ fun AudioRecorder(
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
) {
|
||||
val appSettings =
|
||||
context.dataStore.data.collectAsState(AppSettings.getDefaultInstance()).value
|
||||
|
||||
if (audioRecorder.isInRecording)
|
||||
RecordingStatus(audioRecorder = audioRecorder)
|
||||
else
|
||||
StartRecording(audioRecorder = audioRecorder)
|
||||
StartRecording(audioRecorder = audioRecorder, appSettings = appSettings)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package app.myzel394.alibi.ui.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.dataStore
|
||||
import app.myzel394.alibi.db.AppSettings
|
||||
import app.myzel394.alibi.db.NotificationSettings
|
||||
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.LandingElement
|
||||
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect
|
||||
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput
|
||||
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms.NotificationEditor
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomRecordingNotificationsScreen(
|
||||
navController: NavController,
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||
rememberTopAppBarState()
|
||||
)
|
||||
|
||||
val dataStore = LocalContext.current.dataStore
|
||||
val settings = dataStore
|
||||
.data
|
||||
.collectAsState(initial = AppSettings.getDefaultInstance())
|
||||
.value
|
||||
|
||||
var showEditor: Boolean by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LaunchedEffect(settings.notificationSettings) {
|
||||
if (settings.notificationSettings != null) {
|
||||
showEditor = true
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.ui_settings_option_customNotification_title))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navController::popBackStack) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) { padding ->
|
||||
if (showEditor) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
NotificationEditor(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.padding(vertical = 16.dp),
|
||||
onNotificationChange = { notificationSettings ->
|
||||
scope.launch {
|
||||
dataStore.updateData { settings ->
|
||||
settings.setNotificationSettings(notificationSettings.let {
|
||||
if (it.preset == NotificationSettings.Preset.Default)
|
||||
null
|
||||
else
|
||||
it
|
||||
})
|
||||
}
|
||||
}
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LandingElement(
|
||||
onOpenEditor = {
|
||||
showEditor = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -40,6 +40,7 @@ import app.myzel394.alibi.dataStore
|
||||
import app.myzel394.alibi.db.AppSettings
|
||||
import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY
|
||||
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile
|
||||
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.CustomNotificationTile
|
||||
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile
|
||||
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile
|
||||
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ImportExport
|
||||
@ -146,6 +147,7 @@ fun SettingsScreen(
|
||||
IntervalDurationTile()
|
||||
ForceExactMaxDurationTile()
|
||||
InAppLanguagePicker()
|
||||
CustomNotificationTile(navController = navController)
|
||||
AnimatedVisibility(visible = settings.showAdvancedSettings) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.app.ActivityCompat
|
||||
|
||||
@ -32,3 +33,17 @@ fun Context.openAppSystemSettings() {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
})
|
||||
}
|
||||
|
||||
fun Context.openNotificationsSettings() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startActivity(Intent().apply {
|
||||
action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||
})
|
||||
} else {
|
||||
startActivity(Intent().apply {
|
||||
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
5
app/src/main/res/drawable/ic_cloud.xml
Normal file
5
app/src/main/res/drawable/ic_cloud.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96z"/>
|
||||
</vector>
|
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="1000dp"
|
||||
android:height="1000dp"
|
||||
android:viewportWidth="1000"
|
||||
android:viewportHeight="1000">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M889,646q-41,146 -180,199t-237,-37q-98,-90 -253.5,-119.5t-79.5,-161Q215,396 242.5,249t176,-137q148.5,10 235,98T835,399q95,101 54,247Z"/>
|
||||
<path
|
||||
android:pathData="M889,646q-41,146 -180,199t-237,-37q-98,-90 -253.5,-119.5t-79.5,-161Q215,396 242.5,249t176,-137q148.5,10 235,98T835,399q95,101 54,247Z"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_download.xml
Normal file
5
app/src/main/res/drawable/ic_download.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_note.xml
Normal file
5
app/src/main/res/drawable/ic_note.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_vpn.xml
Normal file
5
app/src/main/res/drawable/ic_vpn.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/launcher_monochrome_noopacity.xml
Normal file
10
app/src/main/res/drawable/launcher_monochrome_noopacity.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="250dp"
|
||||
android:height="250dp"
|
||||
android:viewportWidth="250"
|
||||
android:viewportHeight="250">
|
||||
<path
|
||||
android:pathData="M191,125C191,161.45 161.45,191 125,191C88.55,191 59,161.45 59,125C59,88.55 88.55,59 125,59C161.45,59 191,88.55 191,125ZM110,137C110,142.52 105.52,147 100,147C94.48,147 90,142.52 90,137C90,131.48 94.48,127 100,127C105.52,127 110,131.48 110,137ZM150,147C155.52,147 160,142.52 160,137C160,131.48 155.52,127 150,127C144.48,127 140,131.48 140,137C140,142.52 144.48,147 150,147Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
@ -28,8 +28,11 @@
|
||||
<string name="ui_audioRecorder_action_start_description">Alibi will continue recording in the background and store the last <xliff:g name="durationInMinutes">%s</xliff:g> minutes at your request</string>
|
||||
<string name="ui_audioRecorder_action_save_processing_dialog_title">Processing</string>
|
||||
<string name="ui_audioRecorder_action_save_processing_dialog_description">Processing Audio, do not close Alibi! You will be automatically prompted to save the file once it\'s ready</string>
|
||||
|
||||
<string name="ui_audioRecorder_state_recording_title">Recording Audio</string>
|
||||
<string name="ui_audioRecorder_state_recording_description">Alibi keeps recording in the background</string>
|
||||
<string name="ui_audioRecorder_state_recording_fake_weather_title">Current Weather</string>
|
||||
<string name="ui_audioRecorder_state_recording_fake_weather_description">14° with light chance of rain</string>
|
||||
|
||||
<string name="ui_welcome_explanation_title">Welcome to Alibi!</string>
|
||||
<string name="ui_welcome_explanation_message">Alibi is like a dashcam for your phone. It allows you to record your audio continuously and save the last 30 minutes when you need it.</string>
|
||||
@ -77,4 +80,21 @@
|
||||
<string name="ui_settings_option_import_dialog_text">Are you sure you want to import these settings? Your current settings will be overwritten!</string>
|
||||
<string name="ui_settings_option_import_dialog_confirm">Import settings</string>
|
||||
<string name="ui_settings_option_import_success">Settings have been imported successfully!</string>
|
||||
<string name="ui_settings_option_customNotification_title">Custom Notifications</string>
|
||||
<string name="ui_settings_option_customNotification_description_setup">Setup custom recording notifications now</string>
|
||||
<string name="ui_settings_option_customNotification_description_edit">Edit recording notifications</string>
|
||||
<string name="ui_settings_customNotifications_landing_title">Don\'t expose yourself</string>
|
||||
<string name="ui_settings_customNotifications_landing_description">Due to Android\'s restrictions, Alibi has to show a notification while recording. To hide the fact that you\'re using Alibi, you can customize the notification.</string>
|
||||
<string name="ui_settings_customNotifications_landing_help_hideNotifications">Alternatively, you can also simply disable notifications</string>
|
||||
<string name="ui_settings_customNotifications_landing_getStarted">Create own notification</string>
|
||||
<string name="ui_audioRecorder_state_recording_fake_player_title">Playing Audio</string>
|
||||
<string name="ui_audioRecorder_state_recording_fake_player_description">Now playing: Despacito</string>
|
||||
<string name="ui_audioRecorder_state_recording_fake_browser_title">Downloading attachments.zip</string>
|
||||
<string name="ui_audioRecorder_state_recording_fake_browser_description">Downloading file...</string>
|
||||
<string name="ui_audioRecorder_state_recording_fake_vpn_title">Connected to VPN</string>
|
||||
<string name="ui_audioRecorder_state_recording_fake_vpn_description">Connection Secured</string>
|
||||
<string name="ui_settings_customNotifications_preset_apply_label">Apply Preset \"%s\"</string>
|
||||
<string name="ui_settings_customNotifications_showOngoing_label">Show Duration</string>
|
||||
<string name="ui_settings_customNotifications_save_label">Update notification</string>
|
||||
<string name="ui_settings_customNotifications_edit_help">This is a preview for your notification. You can edit the title and the message. At the bottom you can find some presets.</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user