diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 869d94426..58a0d574f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -331,8 +331,8 @@ fun EditPostView( ImageVideoDescription( url, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent -> - postViewModel.upload(url, alt, sensitiveContent, false, server, accountViewModel::toast, context) + onAdd = { alt, server, sensitiveContent, mediaQuality -> + postViewModel.upload(url, alt, sensitiveContent, mediaQuality, false, server, accountViewModel::toast, context) if (!server.isNip95) { accountViewModel.account.settings.changeDefaultFileServer(server.server) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 9f0c7d45f..b0d3826a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -148,6 +148,7 @@ open class EditPostViewModel : ViewModel() { galleryUri: Uri, alt: String?, sensitiveContent: Boolean, + mediaQuality: Int, isPrivate: Boolean = false, server: ServerOption, onError: (String, String) -> Unit, @@ -223,6 +224,7 @@ open class EditPostViewModel : ViewModel() { isUploadingImage = false onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, it)) }, + mediaQuality = MediaCompressor().intToCompressorQuality(mediaQuality), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 818463a26..0c8ac3137 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -79,6 +79,7 @@ open class NewMediaModel : ViewModel() { fun upload( context: Context, relayList: List? = null, + mediaQuality: Int, onError: (String) -> Unit = {}, ) { isUploadingImage = true @@ -166,6 +167,7 @@ open class NewMediaModel : ViewModel() { uploadingDescription.value = null onError(stringRes(context, R.string.error_when_compressing_media, it)) }, + mediaQuality = MediaCompressor().intToCompressorQuality(mediaQuality), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 9766d25eb..cdce79368 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -44,22 +44,26 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf 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.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -72,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.FileServersEvent import kotlinx.collections.immutable.toImmutableList @@ -100,6 +105,7 @@ fun NewMediaView( var showRelaysDialog by remember { mutableStateOf(false) } var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } + var mediaQualitySlider by remember { mutableIntStateOf(1) } // 0 = Low, 1 = Medium, 2 = High Dialog( onDismissRequest = { onClose() }, @@ -160,7 +166,7 @@ fun NewMediaView( PostButton( onPost = { onClose() - postViewModel.upload(context, relayList) { + postViewModel.upload(context, relayList, mediaQualitySlider) { accountViewModel.toast(stringRes(context, R.string.failed_to_upload_media_no_details), it) } postViewModel.selectedServer?.let { @@ -180,6 +186,59 @@ fun NewMediaView( modifier = Modifier.fillMaxWidth().verticalScroll(scrollState), ) { ImageVideoPost(postViewModel, accountViewModel) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .padding(vertical = 8.dp), + ) { + Column( + modifier = Modifier.weight(1.0f), + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(context, R.string.media_compression_quality_label), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(context, R.string.media_compression_quality_explainer), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + ) + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = + when (mediaQualitySlider) { + 0 -> stringRes(R.string.media_compression_quality_low) + 1 -> stringRes(R.string.media_compression_quality_medium) + 2 -> stringRes(R.string.media_compression_quality_high) + else -> stringRes(R.string.media_compression_quality_medium) + }, + modifier = Modifier.align(Alignment.Center), + ) + } + + Slider( + value = mediaQualitySlider.toFloat(), + onValueChange = { mediaQualitySlider = it.toInt() }, + valueRange = 0f..2f, + steps = 1, + ) + } + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index da3b73200..c55c867c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -88,6 +88,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -493,8 +494,8 @@ fun NewPostView( ImageVideoDescription( url, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent -> - postViewModel.upload(url, alt, sensitiveContent, false, server, accountViewModel::toast, context) + onAdd = { alt, server, sensitiveContent, mediaQuality -> + postViewModel.upload(url, alt, sensitiveContent, mediaQuality, false, server, accountViewModel::toast, context) if (!server.isNip95) { accountViewModel.account.settings.changeDefaultFileServer(server.server) } @@ -1610,7 +1611,7 @@ fun CreateButton( fun ImageVideoDescription( uri: Uri, defaultServer: Nip96MediaServers.ServerName, - onAdd: (String, ServerOption, Boolean) -> Unit, + onAdd: (String, ServerOption, Boolean, Int) -> Unit, onCancel: () -> Unit, onError: (Int) -> Unit, accountViewModel: AccountViewModel, @@ -1668,6 +1669,7 @@ fun ImageVideoDescription( } var message by remember { mutableStateOf("") } var sensitiveContent by remember { mutableStateOf(false) } + var mediaQualitySlider by remember { mutableIntStateOf(1) } // 0 = Low, 1 = Medium, 2 = High Column( modifier = @@ -1846,12 +1848,66 @@ fun ImageVideoDescription( ) } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .padding(vertical = 8.dp), + ) { + Column( + modifier = Modifier.weight(1.0f), + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.media_compression_quality_label), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(R.string.media_compression_quality_explainer), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + ) + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = + when (mediaQualitySlider) { + 0 -> stringRes(R.string.media_compression_quality_low) + 1 -> stringRes(R.string.media_compression_quality_medium) + 2 -> stringRes(R.string.media_compression_quality_high) + else -> stringRes(R.string.media_compression_quality_medium) + }, + modifier = Modifier.align(Alignment.Center), + ) + } + + Slider( + value = mediaQualitySlider.toFloat(), + onValueChange = { mediaQualitySlider = it.toInt() }, + valueRange = 0f..2f, + steps = 1, + ) + } + } + Button( modifier = Modifier .fillMaxWidth() .padding(vertical = 10.dp), - onClick = { onAdd(message, selectedServer, sensitiveContent) }, + onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider) }, shape = QuoteBorder, colors = ButtonDefaults.buttonColors( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 638e0a78b..ad7402f66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -834,6 +834,7 @@ open class NewPostViewModel : ViewModel() { galleryUri: Uri, alt: String?, sensitiveContent: Boolean, + mediaQuality: Int, isPrivate: Boolean = false, server: ServerOption, onError: (title: String, message: String) -> Unit, @@ -909,6 +910,7 @@ open class NewPostViewModel : ViewModel() { isUploadingImage = false onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, it)) }, + mediaQuality = MediaCompressor().intToCompressorQuality(mediaQuality), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index af776e756..055e24eb5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -30,6 +30,7 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip96Uploader +import com.vitorpamplona.amethyst.ui.components.CompressorQuality import com.vitorpamplona.amethyst.ui.components.MediaCompressor import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.events.GitHubIdentity @@ -211,6 +212,8 @@ class NewUserMetadataViewModel : ViewModel() { onError(stringRes(context, R.string.error_when_compressing_media), stringRes(context, it)) }, + // Use MEDIUM quality as default + mediaQuality = CompressorQuality.MEDIUM, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt index 526fd4730..bf53e718d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.components import android.content.Context import android.graphics.Bitmap import android.net.Uri +import android.util.Log import android.webkit.MimeTypeMap import androidx.core.net.toUri import com.abedelazizshe.lightcompressorlibrary.CompressionListener @@ -46,10 +47,20 @@ class MediaCompressor { applicationContext: Context, onReady: (Uri, String?, Long?) -> Unit, onError: (Int) -> Unit, + mediaQuality: CompressorQuality, ) { checkNotInMainThread() if (contentType?.startsWith("video", true) == true) { + val videoQuality = + when (mediaQuality) { + CompressorQuality.VERY_LOW -> VideoQuality.VERY_LOW + CompressorQuality.LOW -> VideoQuality.LOW + CompressorQuality.MEDIUM -> VideoQuality.MEDIUM + CompressorQuality.HIGH -> VideoQuality.HIGH + CompressorQuality.VERY_HIGH -> VideoQuality.VERY_HIGH + } + Log.d("MediaCompressor", "Using video compression $mediaQuality") VideoCompressor.start( // => This is required context = applicationContext, @@ -65,7 +76,7 @@ class MediaCompressor { appSpecificStorageConfiguration = AppSpecificStorageConfiguration(), configureWith = Configuration( - quality = VideoQuality.MEDIUM, + quality = videoQuality, // => required name videoNames = listOf(UUID.randomUUID().toString()), ), @@ -110,10 +121,19 @@ class MediaCompressor { !contentType.contains("gif") && !contentType.contains("svg") ) { + val imageQuality = + when (mediaQuality) { + CompressorQuality.VERY_LOW -> 40 + CompressorQuality.LOW -> 50 + CompressorQuality.MEDIUM -> 60 + CompressorQuality.HIGH -> 80 + CompressorQuality.VERY_HIGH -> 90 + } try { + Log.d("MediaCompressor", "Using image compression $mediaQuality") val compressedImageFile = Compressor.compress(applicationContext, from(uri, contentType, applicationContext)) { - default(width = 640, format = Bitmap.CompressFormat.JPEG) + default(width = 640, format = Bitmap.CompressFormat.JPEG, quality = imageQuality) } onReady(compressedImageFile.toUri(), contentType, compressedImageFile.length()) } catch (e: Exception) { @@ -162,4 +182,28 @@ class MediaCompressor { } return arrayOf(name, extension) } + + fun intToCompressorQuality(mediaQualityFloat: Int): CompressorQuality = + when (mediaQualityFloat) { + 0 -> CompressorQuality.LOW + 1 -> CompressorQuality.MEDIUM + 2 -> CompressorQuality.HIGH + else -> CompressorQuality.MEDIUM + } + + fun compressorQualityToInt(compressorQuality: CompressorQuality): Int = + when (compressorQuality) { + CompressorQuality.LOW -> 0 + CompressorQuality.MEDIUM -> 1 + CompressorQuality.HIGH -> 2 + else -> 1 + } +} + +enum class CompressorQuality { + VERY_LOW, + LOW, + MEDIUM, + HIGH, + VERY_HIGH, } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelScreen.kt index a466c3bcd..f8a7a2337 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelScreen.kt @@ -116,7 +116,9 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.actions.ServerOption import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation +import com.vitorpamplona.amethyst.ui.components.CompressorQuality import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.components.MediaCompressor import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer @@ -514,6 +516,8 @@ fun EditFieldRow( galleryUri = it, alt = null, sensitiveContent = false, + // Use MEDIUM quality + mediaQuality = MediaCompressor().compressorQualityToInt(CompressorQuality.MEDIUM), server = ServerOption(accountViewModel.account.settings.defaultFileServer, false), onError = accountViewModel::toast, context = context, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt index 67c24a611..21d028a95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChatroomScreen.kt @@ -92,6 +92,8 @@ import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.actions.ServerOption import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation +import com.vitorpamplona.amethyst.ui.components.CompressorQuality +import com.vitorpamplona.amethyst.ui.components.MediaCompressor import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture @@ -584,6 +586,8 @@ fun PrivateMessageEditFieldRow( galleryUri = it, alt = null, sensitiveContent = false, + // use MEDIUM quality + mediaQuality = MediaCompressor().compressorQualityToInt(CompressorQuality.MEDIUM), isPrivate = isPrivate, server = ServerOption(accountViewModel.account.settings.defaultFileServer, false), onError = accountViewModel::toast, diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index bc8cc775d..4a84e566d 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -665,6 +665,11 @@ Nelze připravit informace v záhlaví: %1$s Komprese zrušena Komprese se nepodařilo vrátit soubor + Kvalita médií + Vyberte Nízkou kvalitu pro kompresi médií na menší soubor s nižší kvalitou, nebo vyberte Vysokou kvalitu pro kompresi na větší soubor s vyšší kvalitou. + Nízká + Střední + Vysoká Upravit koncept Přihlášení pomocí QR kódu Trasa diff --git a/amethyst/src/main/res/values-de/strings.xml b/amethyst/src/main/res/values-de/strings.xml index 318e70dae..9b0a9134c 100644 --- a/amethyst/src/main/res/values-de/strings.xml +++ b/amethyst/src/main/res/values-de/strings.xml @@ -670,6 +670,11 @@ anz der Bedingungen ist erforderlich Konnte Kopfzeileninformationen nicht vorbereiten: %1$s Komprimierung abgebrochen Komprimierung fehlgeschlagen eine Datei zurückzugeben + Medienqualität + Wählen Sie Niedrige Qualität, um Ihre Medien in eine kleinere Datei mit geringerer Qualität zu komprimieren, oder wählen Sie Hohe Qualität, um sie in eine größere Datei mit höherer Qualität zu komprimieren. + Niedrig + Mittel + Hoch Entwurf bearbeiten Einloggen mit QR-Code Route diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 17899b311..633b4c1e7 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -665,6 +665,11 @@ Não foi possível preparar informações do cabeçalho: %1$s Compressão cancelada Compressão falhou ao retornar um arquivo + Qualidade de Mídia + Selecione Baixa qualidade para comprimir sua mídia para um arquivo menor com menor qualidade ou selecione Alta qualidade para comprimir para um arquivo maior com maior qualidade. + Baixa + Média + Alta Editar rascunho Entrar com Código QR Rota diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index ae3fc86a4..de7fc01e6 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -664,6 +664,11 @@ Kunde inte förbereda header information: %1$s Komprimering avbruten Komprimering misslyckades att returnera en fil + Mediakvalitet + Välj Låg kvalitet för att komprimera ditt media till en mindre fil med lägre kvalitet, eller välj Hög kvalitet för att komprimera till en större fil med högre kvalitet. + Låg + Medel + Hög Redigera utkast Logga in med QR-kod Rutt diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index deb2b0a4f..588ae1a44 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -791,6 +791,12 @@ Compression Cancelled Compression failed to return a file + Media Quality + Select Low quality to compress your media to a smaller file with less quality or select High quality to compress to a larger file with higher quality. + Low + Medium + High + Edit draft Login with QR Code