Compare commits
4 Commits
78e315e6ea
...
ceca568a1a
Author | SHA1 | Date |
---|---|---|
tateisu | ceca568a1a | |
tateisu | 5f7f4c34ec | |
tateisu | 593fffaad2 | |
tateisu | 2ad355b9e2 |
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.20" />
|
<option name="version" value="1.9.22" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -16,7 +16,9 @@
|
||||||
package org.jetbrains.anko
|
package org.jetbrains.anko
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.DimenRes
|
import androidx.annotation.DimenRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -32,25 +34,43 @@ const val XXXHDPI: Int = DisplayMetrics.DENSITY_XXXHIGH
|
||||||
|
|
||||||
const val MAXDPI: Int = 0xfffe
|
const val MAXDPI: Int = 0xfffe
|
||||||
|
|
||||||
//returns dip(dp) dimension value in pixels
|
// sp to px
|
||||||
|
fun DisplayMetrics.sp(sp: Float) =
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, this)
|
||||||
|
|
||||||
|
fun Context.sp(sp: Float) = resources.displayMetrics.sp(sp)
|
||||||
|
fun Context.sp(sp: Int) = resources.displayMetrics.sp(sp.toFloat())
|
||||||
|
|
||||||
|
// px to sp
|
||||||
|
fun DisplayMetrics.px2sp(px: Float): Float = when {
|
||||||
|
Build.VERSION.SDK_INT >= 34 ->
|
||||||
|
TypedValue.deriveDimension(TypedValue.COMPLEX_UNIT_SP, px, this)
|
||||||
|
|
||||||
|
else -> try {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
px / scaledDensity
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.px2sp(px: Float): Float = resources.displayMetrics.px2sp(px)
|
||||||
|
fun Context.px2sp(px: Int): Float = resources.displayMetrics.px2sp(px.toFloat())
|
||||||
|
|
||||||
|
// dip to px
|
||||||
fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt()
|
fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt()
|
||||||
fun Context.dip(value: Float): Int = (value * resources.displayMetrics.density).toInt()
|
fun Context.dip(value: Float): Int = (value * resources.displayMetrics.density).toInt()
|
||||||
|
|
||||||
//return sp dimension value in pixels
|
|
||||||
fun Context.sp(value: Int): Int = (value * resources.displayMetrics.scaledDensity).toInt()
|
|
||||||
fun Context.sp(value: Float): Int = (value * resources.displayMetrics.scaledDensity).toInt()
|
|
||||||
|
|
||||||
//converts px value into dip or sp
|
//converts px value into dip or sp
|
||||||
fun Context.px2dip(px: Int): Float = px.toFloat() / resources.displayMetrics.density
|
fun Context.px2dip(px: Int): Float = px.toFloat() / resources.displayMetrics.density
|
||||||
fun Context.px2sp(px: Int): Float = px.toFloat() / resources.displayMetrics.scaledDensity
|
|
||||||
|
|
||||||
fun Context.dimen(@DimenRes resource: Int): Int = resources.getDimensionPixelSize(resource)
|
fun Context.dimen(@DimenRes resource: Int): Int = resources.getDimensionPixelSize(resource)
|
||||||
|
|
||||||
//the same for nested DSL components
|
//the same for nested DSL components
|
||||||
fun AnkoContext<*>.dip(value: Int): Int = ctx.dip(value)
|
fun AnkoContext<*>.dip(value: Int): Int = ctx.dip(value)
|
||||||
fun AnkoContext<*>.dip(value: Float): Int = ctx.dip(value)
|
fun AnkoContext<*>.dip(value: Float): Int = ctx.dip(value)
|
||||||
fun AnkoContext<*>.sp(value: Int): Int = ctx.sp(value)
|
fun AnkoContext<*>.sp(value: Int) = ctx.sp(value)
|
||||||
fun AnkoContext<*>.sp(value: Float): Int = ctx.sp(value)
|
fun AnkoContext<*>.sp(value: Float) = ctx.sp(value)
|
||||||
fun AnkoContext<*>.px2dip(px: Int): Float = ctx.px2dip(px)
|
fun AnkoContext<*>.px2dip(px: Int): Float = ctx.px2dip(px)
|
||||||
fun AnkoContext<*>.px2sp(px: Int): Float = ctx.px2sp(px)
|
fun AnkoContext<*>.px2sp(px: Int): Float = ctx.px2sp(px)
|
||||||
fun AnkoContext<*>.dimen(@DimenRes resource: Int): Int = ctx.dimen(resource)
|
fun AnkoContext<*>.dimen(@DimenRes resource: Int): Int = ctx.dimen(resource)
|
||||||
|
@ -58,8 +78,8 @@ fun AnkoContext<*>.dimen(@DimenRes resource: Int): Int = ctx.dimen(resource)
|
||||||
//the same for the views
|
//the same for the views
|
||||||
fun View.dip(value: Int): Int = context.dip(value)
|
fun View.dip(value: Int): Int = context.dip(value)
|
||||||
fun View.dip(value: Float): Int = context.dip(value)
|
fun View.dip(value: Float): Int = context.dip(value)
|
||||||
fun View.sp(value: Int): Int = context.sp(value)
|
fun View.sp(value: Int) = context.sp(value)
|
||||||
fun View.sp(value: Float): Int = context.sp(value)
|
fun View.sp(value: Float) = context.sp(value)
|
||||||
fun View.px2dip(px: Int): Float = context.px2dip(px)
|
fun View.px2dip(px: Int): Float = context.px2dip(px)
|
||||||
fun View.px2sp(px: Int): Float = context.px2sp(px)
|
fun View.px2sp(px: Int): Float = context.px2sp(px)
|
||||||
fun View.dimen(@DimenRes resource: Int): Int = context.dimen(resource)
|
fun View.dimen(@DimenRes resource: Int): Int = context.dimen(resource)
|
||||||
|
@ -67,8 +87,8 @@ fun View.dimen(@DimenRes resource: Int): Int = context.dimen(resource)
|
||||||
//the same for Fragments
|
//the same for Fragments
|
||||||
fun Fragment.dip(value: Int): Int = requireContext().dip(value)
|
fun Fragment.dip(value: Int): Int = requireContext().dip(value)
|
||||||
fun Fragment.dip(value: Float): Int = requireContext().dip(value)
|
fun Fragment.dip(value: Float): Int = requireContext().dip(value)
|
||||||
fun Fragment.sp(value: Int): Int = requireContext().sp(value)
|
fun Fragment.sp(value: Int) = requireContext().sp(value)
|
||||||
fun Fragment.sp(value: Float): Int = requireContext().sp(value)
|
fun Fragment.sp(value: Float) = requireContext().sp(value)
|
||||||
fun Fragment.px2dip(px: Int): Float = requireContext().px2dip(px)
|
fun Fragment.px2dip(px: Int): Float = requireContext().px2dip(px)
|
||||||
fun Fragment.px2sp(px: Int): Float = requireContext().px2sp(px)
|
fun Fragment.px2sp(px: Int): Float = requireContext().px2sp(px)
|
||||||
fun Fragment.dimen(@DimenRes resource: Int): Int = requireContext().dimen(resource)
|
fun Fragment.dimen(@DimenRes resource: Int): Int = requireContext().dimen(resource)
|
||||||
|
|
|
@ -45,7 +45,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain( Vers.kotlinJvmToolchain)
|
jvmToolchain(Vers.kotlinJvmToolchain)
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = Vers.kotlinJvmTarget
|
jvmTarget = Vers.kotlinJvmTarget
|
||||||
|
@ -65,5 +65,7 @@ dependencies {
|
||||||
api(project(":apng"))
|
api(project(":apng"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
|
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Vers.desugarLibVersion}")
|
||||||
|
|
||||||
testImplementation("junit:junit:${Vers.junitVersion}")
|
testImplementation("junit:junit:${Vers.junitVersion}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import java.util.Properties
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("org.jetbrains.kotlin.plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
id("io.gitlab.arturbosch.detekt")
|
id("io.gitlab.arturbosch.detekt")
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,6 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// desugar_jdk_libs 2.0.0 は AGP 7.4.0-alpha10 以降を要求する
|
// desugar_jdk_libs 2.0.0 は AGP 7.4.0-alpha10 以降を要求する
|
||||||
|
@ -172,7 +171,8 @@ dependencies {
|
||||||
implementation("com.github.UnifiedPush:android-connector:2.1.1")
|
implementation("com.github.UnifiedPush:android-connector:2.1.1")
|
||||||
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
||||||
|
|
||||||
implementation("com.github.androidmads:QRGenerator:1.0.1")
|
// implementation("com.github.androidmads:QRGenerator:1.0.1")
|
||||||
|
implementation("com.github.alexzhirkevich:custom-qr-generator:1.6.2")
|
||||||
|
|
||||||
val apng4AndroidVersion = "2.25.0"
|
val apng4AndroidVersion = "2.25.0"
|
||||||
implementation("com.github.penfeizhou.android.animation:apng:$apng4AndroidVersion")
|
implementation("com.github.penfeizhou.android.animation:apng:$apng4AndroidVersion")
|
||||||
|
|
|
@ -63,10 +63,14 @@
|
||||||
android:maxSdkVersion="32"
|
android:maxSdkVersion="32"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
|
<!-- Devices running Android 13 (API level 33) or higher -->
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
|
||||||
|
<!-- To handle the reselection within the app on Android 14 (API level 34) -->
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
|
@ -14,43 +14,75 @@ import android.text.Editable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.Spinner
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.jrummyapps.android.colorpicker.ColorPickerDialog
|
import com.jrummyapps.android.colorpicker.ColorPickerDialog
|
||||||
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
|
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
|
||||||
import jp.juggler.subwaytooter.api.*
|
import jp.juggler.subwaytooter.api.TootApiClient
|
||||||
|
import jp.juggler.subwaytooter.api.TootApiResult
|
||||||
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
import jp.juggler.subwaytooter.api.auth.AuthBase
|
import jp.juggler.subwaytooter.api.auth.AuthBase
|
||||||
import jp.juggler.subwaytooter.api.auth.authRepo
|
import jp.juggler.subwaytooter.api.auth.authRepo
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
import jp.juggler.subwaytooter.api.entity.ServiceType
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootAccount
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootAttachment
|
||||||
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
|
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootVisibility
|
||||||
|
import jp.juggler.subwaytooter.api.entity.parseItem
|
||||||
|
import jp.juggler.subwaytooter.api.runApiTask
|
||||||
|
import jp.juggler.subwaytooter.api.runApiTask2
|
||||||
|
import jp.juggler.subwaytooter.api.showApiError
|
||||||
import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding
|
import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding
|
||||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||||
import jp.juggler.subwaytooter.notification.*
|
import jp.juggler.subwaytooter.notification.checkNotificationImmediate
|
||||||
|
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
|
||||||
|
import jp.juggler.subwaytooter.notification.resetNotificationTracking
|
||||||
import jp.juggler.subwaytooter.push.PushBase
|
import jp.juggler.subwaytooter.push.PushBase
|
||||||
import jp.juggler.subwaytooter.push.pushRepo
|
import jp.juggler.subwaytooter.push.pushRepo
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.table.daoAcctColor
|
import jp.juggler.subwaytooter.table.daoAcctColor
|
||||||
import jp.juggler.subwaytooter.table.daoSavedAccount
|
import jp.juggler.subwaytooter.table.daoSavedAccount
|
||||||
import jp.juggler.subwaytooter.util.*
|
import jp.juggler.subwaytooter.util.*
|
||||||
import jp.juggler.util.*
|
import jp.juggler.util.backPressed
|
||||||
import jp.juggler.util.coroutine.AppDispatchers
|
import jp.juggler.util.coroutine.AppDispatchers
|
||||||
import jp.juggler.util.coroutine.launchAndShowError
|
import jp.juggler.util.coroutine.launchAndShowError
|
||||||
import jp.juggler.util.coroutine.launchMain
|
import jp.juggler.util.coroutine.launchMain
|
||||||
import jp.juggler.util.coroutine.launchProgress
|
import jp.juggler.util.coroutine.launchProgress
|
||||||
import jp.juggler.util.data.*
|
import jp.juggler.util.data.UriSerializer
|
||||||
|
import jp.juggler.util.data.getDocumentName
|
||||||
|
import jp.juggler.util.data.getStreamSize
|
||||||
|
import jp.juggler.util.data.notZero
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.log.showToast
|
import jp.juggler.util.log.showToast
|
||||||
import jp.juggler.util.log.withCaption
|
import jp.juggler.util.log.withCaption
|
||||||
|
import jp.juggler.util.long
|
||||||
import jp.juggler.util.media.ResizeConfig
|
import jp.juggler.util.media.ResizeConfig
|
||||||
import jp.juggler.util.media.ResizeType
|
import jp.juggler.util.media.ResizeType
|
||||||
import jp.juggler.util.media.createResizedBitmap
|
import jp.juggler.util.media.createResizedBitmap
|
||||||
import jp.juggler.util.network.toPatch
|
import jp.juggler.util.network.toPatch
|
||||||
import jp.juggler.util.network.toPost
|
import jp.juggler.util.network.toPost
|
||||||
import jp.juggler.util.network.toPostRequestBuilder
|
import jp.juggler.util.network.toPostRequestBuilder
|
||||||
import jp.juggler.util.ui.*
|
import jp.juggler.util.ui.ActivityResultHandler
|
||||||
|
import jp.juggler.util.ui.attrColor
|
||||||
|
import jp.juggler.util.ui.isEnabledAlpha
|
||||||
|
import jp.juggler.util.ui.isOk
|
||||||
|
import jp.juggler.util.ui.scan
|
||||||
|
import jp.juggler.util.ui.vg
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
|
@ -164,61 +196,39 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
loadLanguageList()
|
loadLanguageList()
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private var permissionCamera = permissionSpecCamera.requester {
|
||||||
|
openCamera()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pickImageLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||||
|
|
||||||
|
private val pickImageCallback = ActivityResultCallback<Uri?> {
|
||||||
|
handlePickImageResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val arCameraImage = ActivityResultHandler(log) {
|
||||||
|
handleCameraResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
private val arShowAcctColor = ActivityResultHandler(log) { r ->
|
private val arShowAcctColor = ActivityResultHandler(log) { r ->
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
if (r.isOk) showAcctColor()
|
||||||
showAcctColor()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val arAddAttachment = ActivityResultHandler(log) { r ->
|
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
|
||||||
r.data
|
|
||||||
?.handleGetContentResult(contentResolver)
|
|
||||||
?.firstOrNull()
|
|
||||||
?.let {
|
|
||||||
uploadImage(
|
|
||||||
state.propName,
|
|
||||||
it.uri,
|
|
||||||
it.uri.resolveMimeType(it.mimeType, this),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val arCameraImage = ActivityResultHandler(log) { r ->
|
|
||||||
if (r.isNotOk) {
|
|
||||||
// 失敗したら DBからデータを削除
|
|
||||||
state.uriCameraImage?.let {
|
|
||||||
contentResolver.delete(it, null, null)
|
|
||||||
}
|
|
||||||
state.uriCameraImage = null
|
|
||||||
} else {
|
|
||||||
// 画像のURL
|
|
||||||
val uri = r.data?.data ?: state.uriCameraImage
|
|
||||||
if (uri != null) {
|
|
||||||
uploadImage(
|
|
||||||
state.propName,
|
|
||||||
uri,
|
|
||||||
uri.resolveMimeType(null, this),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val prPickAvater = permissionSpecImagePicker.requester { openPicker(it) }
|
|
||||||
private val prPickHeader = permissionSpecImagePicker.requester { openPicker(it) }
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////
|
///////////////////////////////////////////////////
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
backPressed { handleBackPressed() }
|
backPressed { handleBackPressed() }
|
||||||
|
|
||||||
prPickAvater.register(this)
|
pickImageLauncher = registerForActivityResult(
|
||||||
prPickHeader.register(this)
|
ActivityResultContracts.PickVisualMedia(),
|
||||||
|
pickImageCallback,
|
||||||
|
)
|
||||||
|
|
||||||
|
permissionCamera.register(this)
|
||||||
arShowAcctColor.register(this)
|
arShowAcctColor.register(this)
|
||||||
arAddAttachment.register(this)
|
|
||||||
arCameraImage.register(this)
|
arCameraImage.register(this)
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
|
@ -1335,64 +1345,89 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pickAvatarImage() {
|
private fun pickAvatarImage() {
|
||||||
openPicker(prPickAvater)
|
openImagePickerOrCamera("avatar")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pickHeaderImage() {
|
private fun pickHeaderImage() {
|
||||||
openPicker(prPickHeader)
|
openImagePickerOrCamera("header")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openPicker(permissionRequester: PermissionRequester) {
|
private fun openImagePickerOrCamera(propName: String) {
|
||||||
|
state.propName = propName
|
||||||
launchAndShowError {
|
launchAndShowError {
|
||||||
if (!permissionRequester.checkOrLaunch()) return@launchAndShowError
|
|
||||||
val propName = when (permissionRequester) {
|
|
||||||
prPickHeader -> "header"
|
|
||||||
else -> "avatar"
|
|
||||||
}
|
|
||||||
actionsDialog {
|
actionsDialog {
|
||||||
action(getString(R.string.pick_image)) {
|
action(getString(R.string.pick_image)) {
|
||||||
performAttachment(propName)
|
openPickImage()
|
||||||
}
|
}
|
||||||
action(getString(R.string.image_capture)) {
|
action(getString(R.string.image_capture)) {
|
||||||
performCamera(propName)
|
openCamera()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performAttachment(propName: String) {
|
private fun openPickImage() {
|
||||||
try {
|
(pickImageLauncher ?: error("pickImageLauncher not registered")).launch(
|
||||||
state.propName = propName
|
PickVisualMediaRequest(
|
||||||
val intent = intentGetContent(false, getString(R.string.pick_image), arrayOf("image/*"))
|
ActivityResultContracts.PickVisualMedia.ImageOnly,
|
||||||
arAddAttachment.launch(intent)
|
)
|
||||||
} catch (ex: Throwable) {
|
)
|
||||||
log.e(ex, "performAttachment failed.")
|
|
||||||
showToast(ex, "performAttachment failed.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performCamera(propName: String) {
|
private fun handlePickImageResult(uri: Uri?) {
|
||||||
|
uri ?: return
|
||||||
|
uploadImage(
|
||||||
|
state.propName,
|
||||||
|
uri,
|
||||||
|
uri.resolveMimeType(null, this),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
private fun openCamera() {
|
||||||
|
if (!permissionCamera.checkOrLaunch()) return
|
||||||
|
launchAndShowError(errorCaption = "openCamera failed.") {
|
||||||
// カメラで撮影
|
// カメラで撮影
|
||||||
val filename = System.currentTimeMillis().toString() + ".jpg"
|
val filename = System.currentTimeMillis().toString() + ".jpg"
|
||||||
val values = ContentValues()
|
val values = ContentValues().apply {
|
||||||
values.put(MediaStore.Images.Media.TITLE, filename)
|
put(MediaStore.Images.Media.TITLE, filename)
|
||||||
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||||
|
}
|
||||||
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||||
state.uriCameraImage = uri
|
.also { state.uriCameraImage = it }
|
||||||
|
|
||||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
|
putExtra(MediaStore.EXTRA_OUTPUT, uri)
|
||||||
|
}
|
||||||
state.propName = propName
|
|
||||||
arCameraImage.launch(intent)
|
arCameraImage.launch(intent)
|
||||||
} catch (ex: Throwable) {
|
|
||||||
log.e(ex, "opening camera app failed.")
|
|
||||||
showToast(ex, "opening camera app failed.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCameraResult(r: ActivityResult) {
|
||||||
|
when {
|
||||||
|
r.isOk -> {
|
||||||
|
// 画像のURL
|
||||||
|
val uri = r.data?.data ?: state.uriCameraImage
|
||||||
|
if (uri != null) {
|
||||||
|
uploadImage(
|
||||||
|
state.propName,
|
||||||
|
uri,
|
||||||
|
uri.resolveMimeType(null, this),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// 失敗したら DBからデータを削除
|
||||||
|
state.uriCameraImage?.let {
|
||||||
|
contentResolver.delete(it, null, null)
|
||||||
|
}
|
||||||
|
state.uriCameraImage = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
|
||||||
internal interface InputStreamOpener {
|
internal interface InputStreamOpener {
|
||||||
val mimeType: String
|
val mimeType: String
|
||||||
val uri: Uri
|
val uri: Uri
|
||||||
|
|
|
@ -22,7 +22,6 @@ import jp.juggler.subwaytooter.actpost.CompletionHelper
|
||||||
import jp.juggler.subwaytooter.actpost.FeaturedTagCache
|
import jp.juggler.subwaytooter.actpost.FeaturedTagCache
|
||||||
import jp.juggler.subwaytooter.actpost.addAttachment
|
import jp.juggler.subwaytooter.actpost.addAttachment
|
||||||
import jp.juggler.subwaytooter.actpost.applyMushroomText
|
import jp.juggler.subwaytooter.actpost.applyMushroomText
|
||||||
import jp.juggler.subwaytooter.actpost.rearrangeAttachments
|
|
||||||
import jp.juggler.subwaytooter.actpost.onPickCustomThumbnailImpl
|
import jp.juggler.subwaytooter.actpost.onPickCustomThumbnailImpl
|
||||||
import jp.juggler.subwaytooter.actpost.onPostAttachmentCompleteImpl
|
import jp.juggler.subwaytooter.actpost.onPostAttachmentCompleteImpl
|
||||||
import jp.juggler.subwaytooter.actpost.openAttachment
|
import jp.juggler.subwaytooter.actpost.openAttachment
|
||||||
|
@ -33,6 +32,7 @@ import jp.juggler.subwaytooter.actpost.performAttachmentClick
|
||||||
import jp.juggler.subwaytooter.actpost.performMore
|
import jp.juggler.subwaytooter.actpost.performMore
|
||||||
import jp.juggler.subwaytooter.actpost.performPost
|
import jp.juggler.subwaytooter.actpost.performPost
|
||||||
import jp.juggler.subwaytooter.actpost.performSchedule
|
import jp.juggler.subwaytooter.actpost.performSchedule
|
||||||
|
import jp.juggler.subwaytooter.actpost.rearrangeAttachments
|
||||||
import jp.juggler.subwaytooter.actpost.removeReply
|
import jp.juggler.subwaytooter.actpost.removeReply
|
||||||
import jp.juggler.subwaytooter.actpost.resetSchedule
|
import jp.juggler.subwaytooter.actpost.resetSchedule
|
||||||
import jp.juggler.subwaytooter.actpost.restoreState
|
import jp.juggler.subwaytooter.actpost.restoreState
|
||||||
|
@ -82,7 +82,7 @@ import java.util.concurrent.ConcurrentHashMap
|
||||||
class ActPost : AppCompatActivity(),
|
class ActPost : AppCompatActivity(),
|
||||||
View.OnClickListener,
|
View.OnClickListener,
|
||||||
PostAttachment.Callback,
|
PostAttachment.Callback,
|
||||||
MyClickableSpanHandler, AttachmentPicker.Callback {
|
MyClickableSpanHandler {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LogCategory("ActPost")
|
private val log = LogCategory("ActPost")
|
||||||
|
@ -215,7 +215,21 @@ class ActPost : AppCompatActivity(),
|
||||||
appState = App1.getAppState(this)
|
appState = App1.getAppState(this)
|
||||||
handler = appState.handler
|
handler = appState.handler
|
||||||
attachmentUploader = AttachmentUploader(this, handler)
|
attachmentUploader = AttachmentUploader(this, handler)
|
||||||
attachmentPicker = AttachmentPicker(this, this)
|
attachmentPicker = AttachmentPicker(this, object : AttachmentPicker.Callback {
|
||||||
|
override suspend fun onPickAttachment(uri: Uri, mimeType: String?) {
|
||||||
|
addAttachment(uri, mimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPickCustomThumbnail(
|
||||||
|
attachmentId: String?,
|
||||||
|
src: GetContentResultEntry,
|
||||||
|
) {
|
||||||
|
val pa = attachmentList.find { it.attachment?.id?.toString() == attachmentId }
|
||||||
|
?: error("missing attachment for attachmentId=$attachmentId")
|
||||||
|
onPickCustomThumbnailImpl(pa, src)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
density = resources.displayMetrics.density
|
density = resources.displayMetrics.density
|
||||||
arMushroom.register(this)
|
arMushroom.register(this)
|
||||||
|
|
||||||
|
@ -315,6 +329,7 @@ class ActPost : AppCompatActivity(),
|
||||||
R.id.btnFeaturedTag -> completionHelper.openFeaturedTagList(
|
R.id.btnFeaturedTag -> completionHelper.openFeaturedTagList(
|
||||||
featuredTagCache[account?.acct?.ascii ?: ""]?.list
|
featuredTagCache[account?.acct?.ascii ?: ""]?.list
|
||||||
)
|
)
|
||||||
|
|
||||||
R.id.btnAttachmentsRearrange -> rearrangeAttachments()
|
R.id.btnAttachmentsRearrange -> rearrangeAttachments()
|
||||||
R.id.ibSchedule -> performSchedule()
|
R.id.ibSchedule -> performSchedule()
|
||||||
R.id.ibScheduleReset -> resetSchedule()
|
R.id.ibScheduleReset -> resetSchedule()
|
||||||
|
@ -337,10 +352,6 @@ class ActPost : AppCompatActivity(),
|
||||||
openBrowser(span.linkInfo.url)
|
openBrowser(span.linkInfo.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPickAttachment(uri: Uri, mimeType: String?) {
|
|
||||||
addAttachment(uri, mimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostAttachmentProgress() {
|
override fun onPostAttachmentProgress() {
|
||||||
launchIO {
|
launchIO {
|
||||||
try {
|
try {
|
||||||
|
@ -355,15 +366,6 @@ class ActPost : AppCompatActivity(),
|
||||||
onPostAttachmentCompleteImpl(pa)
|
onPostAttachmentCompleteImpl(pa)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resumeCustomThumbnailTarget(id: String?): PostAttachment? {
|
|
||||||
id ?: return null
|
|
||||||
return attachmentList.find { it.attachment?.id?.toString() == id }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPickCustomThumbnail(pa: PostAttachment, src: GetContentResultEntry) {
|
|
||||||
onPickCustomThumbnailImpl(pa, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initUI() {
|
fun initUI() {
|
||||||
setContentView(views.root)
|
setContentView(views.root)
|
||||||
|
|
||||||
|
|
|
@ -269,7 +269,9 @@ fun ActPost.performAttachmentClick(idx: Int) {
|
||||||
TootAttachmentType.GIFV,
|
TootAttachmentType.GIFV,
|
||||||
TootAttachmentType.Video,
|
TootAttachmentType.Video,
|
||||||
-> action(getString(R.string.custom_thumbnail)) {
|
-> action(getString(R.string.custom_thumbnail)) {
|
||||||
attachmentPicker.openCustomThumbnail(pa)
|
attachmentPicker.openCustomThumbnail(
|
||||||
|
attachmentId = pa.attachment?.id?.toString()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
|
|
|
@ -1,95 +1,101 @@
|
||||||
package jp.juggler.subwaytooter.dialog
|
package jp.juggler.subwaytooter.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.View
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.widget.ImageView
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.widget.TextView
|
import com.github.alexzhirkevich.customqrgenerator.QrData
|
||||||
import androidmads.library.qrgenearator.QRGContents
|
import com.github.alexzhirkevich.customqrgenerator.vector.QrCodeDrawable
|
||||||
import androidmads.library.qrgenearator.QRGEncoder
|
import com.github.alexzhirkevich.customqrgenerator.vector.createQrVectorOptions
|
||||||
import jp.juggler.subwaytooter.ActMain
|
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorBallShape
|
||||||
|
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorColor
|
||||||
|
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorFrameShape
|
||||||
|
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogoPadding
|
||||||
|
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogoShape
|
||||||
|
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorPixelShape
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.util.coroutine.launchProgress
|
import jp.juggler.subwaytooter.databinding.DlgQrCodeBinding
|
||||||
|
import jp.juggler.util.coroutine.AppDispatchers
|
||||||
|
import jp.juggler.util.coroutine.launchAndShowError
|
||||||
|
import jp.juggler.util.coroutine.withProgress
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
|
import jp.juggler.util.os.resDrawable
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
private val log = LogCategory("DlgQRCode")
|
||||||
object DlgQRCode {
|
|
||||||
|
|
||||||
private val log = LogCategory("DlgQRCode")
|
val UInt.int get() = toInt()
|
||||||
|
|
||||||
internal interface QrCodeCallback {
|
fun AppCompatActivity.dialogQrCode(
|
||||||
fun onQrCode(bitmap: Bitmap?)
|
message: CharSequence,
|
||||||
}
|
url: String,
|
||||||
|
) = launchAndShowError("dialogQrCode failed.") {
|
||||||
private fun makeQrCode(
|
val drawable = withProgress(
|
||||||
activity: ActMain,
|
caption = getString(R.string.generating_qr_code),
|
||||||
size: Int,
|
|
||||||
url: String,
|
|
||||||
callback: QrCodeCallback,
|
|
||||||
) {
|
) {
|
||||||
activity.launchProgress(
|
withContext(AppDispatchers.DEFAULT) {
|
||||||
"making QR code",
|
QrCodeDrawable(data = QrData.Url(url), options = qrCodeOptions())
|
||||||
progressInitializer = {
|
}
|
||||||
it.setMessageEx(activity.getString(R.string.generating_qr_code))
|
}
|
||||||
},
|
val dialog = Dialog(this@dialogQrCode)
|
||||||
doInBackground = {
|
|
||||||
try {
|
val views = DlgQrCodeBinding.inflate(layoutInflater).apply {
|
||||||
QRGEncoder(
|
btnCancel.setOnClickListener { dialog.cancel() }
|
||||||
/* data */ url,
|
ivQrCode.setImageDrawable(drawable)
|
||||||
/* bundle */ null,
|
tvMessage.text = message
|
||||||
QRGContents.Type.TEXT,
|
tvUrl.text = "[ $url ]" // なぜか素のURLだと@以降が表示されない
|
||||||
/* dimension */ size,
|
|
||||||
).apply {
|
|
||||||
// 背景色
|
|
||||||
colorBlack = Color.WHITE
|
|
||||||
// 図柄の色
|
|
||||||
colorWhite = Color.BLACK
|
|
||||||
}.bitmap
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
log.e(ex, "QR generation failed.")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
afterProc = {
|
|
||||||
if (it != null) callback.onQrCode(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun open(activity: ActMain, message: CharSequence, url: String) {
|
dialog.apply {
|
||||||
|
setContentView(views.root)
|
||||||
val size = (0.5f + 240f * activity.density).toInt()
|
setCancelable(true)
|
||||||
makeQrCode(activity, size, url, object : QrCodeCallback {
|
setCanceledOnTouchOutside(true)
|
||||||
|
show()
|
||||||
@SuppressLint("InflateParams")
|
}
|
||||||
override fun onQrCode(bitmap: Bitmap?) {
|
}
|
||||||
|
|
||||||
val viewRoot = activity.layoutInflater.inflate(R.layout.dlg_qr_code, null, false)
|
private fun AppCompatActivity.qrCodeOptions() = createQrVectorOptions {
|
||||||
val dialog = Dialog(activity)
|
background {
|
||||||
dialog.setContentView(viewRoot)
|
drawable = ColorDrawable(Color.WHITE)
|
||||||
dialog.setCancelable(true)
|
}
|
||||||
dialog.setCanceledOnTouchOutside(true)
|
|
||||||
|
padding = .125f
|
||||||
var tv = viewRoot.findViewById<TextView>(R.id.tvMessage)
|
|
||||||
tv.text = message
|
logo {
|
||||||
|
drawable = resDrawable(R.drawable.qr_code_center)
|
||||||
tv = viewRoot.findViewById(R.id.tvUrl)
|
size = .25f
|
||||||
tv.text = "[ $url ]" // なぜか素のURLだと@以降が表示されない
|
shape = QrVectorLogoShape.Default
|
||||||
|
padding = QrVectorLogoPadding.Natural(.1f)
|
||||||
val iv = viewRoot.findViewById<ImageView>(R.id.ivQrCode)
|
}
|
||||||
iv.setImageBitmap(bitmap)
|
shapes {
|
||||||
|
// 市松模様のドット
|
||||||
dialog.setOnDismissListener {
|
darkPixel = QrVectorPixelShape.RoundCorners(.5f)
|
||||||
iv.setImageDrawable(null)
|
// 3隅の真ん中の大きめドット
|
||||||
bitmap?.recycle()
|
ball = QrVectorBallShape.RoundCorners(.25f)
|
||||||
}
|
// 3隅の枠
|
||||||
|
frame = QrVectorFrameShape.RoundCorners(.25f)
|
||||||
viewRoot.findViewById<View>(R.id.btnCancel).setOnClickListener { dialog.cancel() }
|
}
|
||||||
|
colors {
|
||||||
dialog.show()
|
val cobalt = 0xFF0088FFU.int
|
||||||
}
|
val cobaltDark = 0xFF004488U.int
|
||||||
})
|
// 市松模様のドット
|
||||||
|
dark = QrVectorColor.Solid(cobaltDark)
|
||||||
|
// 3隅の真ん中の大きめドット
|
||||||
|
ball = QrVectorColor.RadialGradient(
|
||||||
|
colors = listOf(
|
||||||
|
0f to cobaltDark,
|
||||||
|
1f to cobalt,
|
||||||
|
),
|
||||||
|
radius = 2f,
|
||||||
|
)
|
||||||
|
// 3隅の枠
|
||||||
|
frame = QrVectorColor.LinearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
0f to cobaltDark,
|
||||||
|
1f to cobalt,
|
||||||
|
),
|
||||||
|
orientation = QrVectorColor.LinearGradient
|
||||||
|
.Orientation.Vertical
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import jp.juggler.subwaytooter.column.Column
|
||||||
import jp.juggler.subwaytooter.column.ColumnType
|
import jp.juggler.subwaytooter.column.ColumnType
|
||||||
import jp.juggler.subwaytooter.databinding.DlgContextMenuBinding
|
import jp.juggler.subwaytooter.databinding.DlgContextMenuBinding
|
||||||
import jp.juggler.subwaytooter.dialog.DlgListMember
|
import jp.juggler.subwaytooter.dialog.DlgListMember
|
||||||
import jp.juggler.subwaytooter.dialog.DlgQRCode
|
import jp.juggler.subwaytooter.dialog.dialogQrCode
|
||||||
import jp.juggler.subwaytooter.pref.PrefB
|
import jp.juggler.subwaytooter.pref.PrefB
|
||||||
import jp.juggler.subwaytooter.pref.PrefI
|
import jp.juggler.subwaytooter.pref.PrefI
|
||||||
import jp.juggler.subwaytooter.span.MyClickableSpan
|
import jp.juggler.subwaytooter.span.MyClickableSpan
|
||||||
|
@ -446,11 +446,11 @@ internal class DlgContextMenu(
|
||||||
else -> whoHost
|
else -> whoHost
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateGroup(btn: Button, group: View, toggle: Boolean = false): Boolean {
|
private fun updateGroup(btn: Button, group: View, toggle: Boolean = false) {
|
||||||
|
|
||||||
if (btn.visibility != View.VISIBLE) {
|
if (btn.visibility != View.VISIBLE) {
|
||||||
group.vg(false)
|
group.vg(false)
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
@ -463,51 +463,57 @@ internal class DlgContextMenu(
|
||||||
else -> btn.setOnClickListener(this)
|
else -> btn.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
val iconId = if (group.visibility == View.VISIBLE) {
|
val iconId = when (group.visibility) {
|
||||||
R.drawable.ic_arrow_drop_up
|
View.VISIBLE -> R.drawable.ic_arrow_drop_up
|
||||||
} else {
|
else -> R.drawable.ic_arrow_drop_down
|
||||||
R.drawable.ic_arrow_drop_down
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val iconColor = activity.attrColor(R.attr.colorTimeSmall)
|
val iconColor = activity.attrColor(R.attr.colorTimeSmall)
|
||||||
val drawable = createColoredDrawable(activity, iconId, iconColor, 1f)
|
val drawable = createColoredDrawable(activity, iconId, iconColor, 1f)
|
||||||
btn.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null)
|
btn.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onClickUpdateGroup(v: View): Boolean = when (v.id) {
|
private fun onClickUpdateGroup(v: View): Boolean {
|
||||||
R.id.btnGroupStatusCrossAccount -> updateGroup(
|
when (v.id) {
|
||||||
views.btnGroupStatusCrossAccount,
|
R.id.btnGroupStatusCrossAccount -> updateGroup(
|
||||||
views.llGroupStatusCrossAccount,
|
views.btnGroupStatusCrossAccount,
|
||||||
toggle = true
|
views.llGroupStatusCrossAccount,
|
||||||
)
|
toggle = true
|
||||||
|
)
|
||||||
|
|
||||||
R.id.btnGroupUserCrossAccount -> updateGroup(
|
R.id.btnGroupUserCrossAccount -> updateGroup(
|
||||||
views.btnGroupUserCrossAccount,
|
views.btnGroupUserCrossAccount,
|
||||||
views.llGroupUserCrossAccount,
|
views.llGroupUserCrossAccount,
|
||||||
toggle = true
|
toggle = true
|
||||||
)
|
)
|
||||||
R.id.btnGroupStatusAround -> updateGroup(
|
|
||||||
views.btnGroupStatusAround,
|
R.id.btnGroupStatusAround -> updateGroup(
|
||||||
views.llGroupStatusAround,
|
views.btnGroupStatusAround,
|
||||||
toggle = true
|
views.llGroupStatusAround,
|
||||||
)
|
toggle = true
|
||||||
R.id.btnGroupStatusByMe -> updateGroup(
|
)
|
||||||
views.btnGroupStatusByMe,
|
|
||||||
views.llGroupStatusByMe,
|
R.id.btnGroupStatusByMe -> updateGroup(
|
||||||
toggle = true
|
views.btnGroupStatusByMe,
|
||||||
)
|
views.llGroupStatusByMe,
|
||||||
R.id.btnGroupStatusExtra -> updateGroup(
|
toggle = true
|
||||||
views.btnGroupStatusExtra,
|
)
|
||||||
views.llGroupStatusExtra,
|
|
||||||
toggle = true
|
R.id.btnGroupStatusExtra -> updateGroup(
|
||||||
)
|
views.btnGroupStatusExtra,
|
||||||
R.id.btnGroupUserExtra -> updateGroup(
|
views.llGroupStatusExtra,
|
||||||
views.btnGroupUserExtra,
|
toggle = true
|
||||||
views.llGroupUserExtra,
|
)
|
||||||
toggle = true
|
|
||||||
)
|
R.id.btnGroupUserExtra -> updateGroup(
|
||||||
else -> false
|
views.btnGroupUserExtra,
|
||||||
|
views.llGroupUserExtra,
|
||||||
|
toggle = true
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ActMain.onClickUserAndStatus(
|
private fun ActMain.onClickUserAndStatus(
|
||||||
|
@ -552,16 +558,18 @@ internal class DlgContextMenu(
|
||||||
accessInfo,
|
accessInfo,
|
||||||
who
|
who
|
||||||
)
|
)
|
||||||
|
|
||||||
R.id.btnNickname -> clickNicknameCustomize(accessInfo, who)
|
R.id.btnNickname -> clickNicknameCustomize(accessInfo, who)
|
||||||
R.id.btnAccountQrCode -> DlgQRCode.open(
|
R.id.btnAccountQrCode -> activity.dialogQrCode(
|
||||||
activity,
|
message = whoRef.decoded_display_name,
|
||||||
whoRef.decoded_display_name,
|
url = who.getUserUrl()
|
||||||
who.getUserUrl()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
R.id.btnDomainBlock -> clickDomainBlock(accessInfo, who)
|
R.id.btnDomainBlock -> clickDomainBlock(accessInfo, who)
|
||||||
R.id.btnOpenTimeline -> who.apiHost.valid()?.let { timelineLocal(pos, it) }
|
R.id.btnOpenTimeline -> who.apiHost.valid()?.let { timelineLocal(pos, it) }
|
||||||
R.id.btnDomainTimeline -> who.apiHost.valid()
|
R.id.btnDomainTimeline -> who.apiHost.valid()
|
||||||
?.let { timelineDomain(pos, accessInfo, it) }
|
?.let { timelineDomain(pos, accessInfo, it) }
|
||||||
|
|
||||||
R.id.btnAvatarImage -> openAvatarImage(who)
|
R.id.btnAvatarImage -> openAvatarImage(who)
|
||||||
R.id.btnQuoteName -> quoteName(who)
|
R.id.btnQuoteName -> quoteName(who)
|
||||||
R.id.btnHideBoost -> userSetShowBoosts(accessInfo, who, false)
|
R.id.btnHideBoost -> userSetShowBoosts(accessInfo, who, false)
|
||||||
|
@ -574,6 +582,7 @@ internal class DlgContextMenu(
|
||||||
column,
|
column,
|
||||||
getUserApiHost()
|
getUserApiHost()
|
||||||
)
|
)
|
||||||
|
|
||||||
R.id.btnEndorse -> userEndorsement(accessInfo, who, !relation.endorsed)
|
R.id.btnEndorse -> userEndorsement(accessInfo, who, !relation.endorsed)
|
||||||
R.id.btnCopyAccountId -> who.id.toString().copyToClipboard(activity)
|
R.id.btnCopyAccountId -> who.id.toString().copyToClipboard(activity)
|
||||||
R.id.btnOpenAccountInAdminWebUi -> openBrowser("https://${accessInfo.apiHost.ascii}/admin/accounts/${who.id}")
|
R.id.btnOpenAccountInAdminWebUi -> openBrowser("https://${accessInfo.apiHost.ascii}/admin/accounts/${who.id}")
|
||||||
|
@ -612,6 +621,7 @@ internal class DlgContextMenu(
|
||||||
accessInfo,
|
accessInfo,
|
||||||
status
|
status
|
||||||
)
|
)
|
||||||
|
|
||||||
R.id.btnQuoteUrlStatus -> openPost(status.url?.notEmpty())
|
R.id.btnQuoteUrlStatus -> openPost(status.url?.notEmpty())
|
||||||
R.id.btnShareUrlStatus -> shareText(status.url?.notEmpty())
|
R.id.btnShareUrlStatus -> shareText(status.url?.notEmpty())
|
||||||
R.id.btnConversationMute -> conversationMute(accessInfo, status)
|
R.id.btnConversationMute -> conversationMute(accessInfo, status)
|
||||||
|
@ -622,6 +632,7 @@ internal class DlgContextMenu(
|
||||||
accessInfo,
|
accessInfo,
|
||||||
status
|
status
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -675,10 +686,12 @@ internal class DlgContextMenu(
|
||||||
dialog.dismissSafe()
|
dialog.dismissSafe()
|
||||||
followFromAnotherAccount(pos, accessInfo, who)
|
followFromAnotherAccount(pos, accessInfo, who)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.btnProfile -> {
|
R.id.btnProfile -> {
|
||||||
dialog.dismissSafe()
|
dialog.dismissSafe()
|
||||||
userProfileFromAnotherAccount(pos, accessInfo, who)
|
userProfileFromAnotherAccount(pos, accessInfo, who)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.btnSendMessage -> {
|
R.id.btnSendMessage -> {
|
||||||
dialog.dismissSafe()
|
dialog.dismissSafe()
|
||||||
mentionFromAnotherAccount(accessInfo, who)
|
mentionFromAnotherAccount(accessInfo, who)
|
||||||
|
|
|
@ -20,7 +20,7 @@ import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.log.errorEx
|
import jp.juggler.util.log.errorEx
|
||||||
import jp.juggler.util.log.showToast
|
import jp.juggler.util.log.showToast
|
||||||
import jp.juggler.util.log.withCaption
|
import jp.juggler.util.log.withCaption
|
||||||
import jp.juggler.util.media.MovideResizeMode
|
import jp.juggler.util.media.MovieResizeMode
|
||||||
import jp.juggler.util.media.MovieResizeConfig
|
import jp.juggler.util.media.MovieResizeConfig
|
||||||
import jp.juggler.util.media.ResizeConfig
|
import jp.juggler.util.media.ResizeConfig
|
||||||
import jp.juggler.util.media.ResizeType
|
import jp.juggler.util.media.ResizeType
|
||||||
|
@ -928,7 +928,7 @@ class SavedAccount(
|
||||||
|
|
||||||
fun getMovieResizeConfig() =
|
fun getMovieResizeConfig() =
|
||||||
MovieResizeConfig(
|
MovieResizeConfig(
|
||||||
mode = MovideResizeMode.fromInt(movieTranscodeMode),
|
mode = MovieResizeMode.fromInt(movieTranscodeMode),
|
||||||
limitBitrate = movieTranscodeBitrate.toLongOrNull()
|
limitBitrate = movieTranscodeBitrate.toLongOrNull()
|
||||||
?.takeIf { it >= 100_000L } ?: 2_000_000L,
|
?.takeIf { it >= 100_000L } ?: 2_000_000L,
|
||||||
limitFrameRate = movieTranscodeFramerate.toIntOrNull()
|
limitFrameRate = movieTranscodeFramerate.toIntOrNull()
|
||||||
|
|
|
@ -4,6 +4,11 @@ import android.content.ContentValues
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||||
|
@ -17,8 +22,8 @@ import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.log.showToast
|
import jp.juggler.util.log.showToast
|
||||||
import jp.juggler.util.ui.ActivityResultHandler
|
import jp.juggler.util.ui.ActivityResultHandler
|
||||||
import jp.juggler.util.ui.isNotOk
|
import jp.juggler.util.ui.isNotOk
|
||||||
|
import jp.juggler.util.ui.isOk
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
|
||||||
class AttachmentPicker(
|
class AttachmentPicker(
|
||||||
|
@ -33,9 +38,8 @@ class AttachmentPicker(
|
||||||
|
|
||||||
// callback after media selected
|
// callback after media selected
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onPickAttachment(uri: Uri, mimeType: String? = null)
|
suspend fun onPickAttachment(uri: Uri, mimeType: String? = null)
|
||||||
fun onPickCustomThumbnail(pa: PostAttachment, src: GetContentResultEntry)
|
suspend fun onPickCustomThumbnail(attachmentId: String?, src: GetContentResultEntry)
|
||||||
fun resumeCustomThumbnailTarget(id: String?): PostAttachment?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// actions after permission granted
|
// actions after permission granted
|
||||||
|
@ -43,7 +47,6 @@ class AttachmentPicker(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class States(
|
data class States(
|
||||||
|
|
||||||
@Serializable(with = UriSerializer::class)
|
@Serializable(with = UriSerializer::class)
|
||||||
var uriCameraImage: Uri? = null,
|
var uriCameraImage: Uri? = null,
|
||||||
|
|
||||||
|
@ -55,59 +58,48 @@ class AttachmentPicker(
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
// activity result handlers
|
// activity result handlers
|
||||||
|
|
||||||
private val arAttachmentChooser = ActivityResultHandler(log) { r ->
|
private var pickThumbnailLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||||
|
|
||||||
|
private val pickThumbnailCallback = ActivityResultCallback<Uri?> {
|
||||||
|
handleThumbnailResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pickVisualMediaLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||||
|
|
||||||
|
private val pickVisualMediaCallback = ActivityResultCallback<List<Uri>?> { uris ->
|
||||||
|
uris?.handleGetContentResult(activity.contentResolver)?.pickAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val prPickAudio = permissionSpecAudioPicker.requester { openAudioPicker() }
|
||||||
|
private val arPickAudio = ActivityResultHandler(log) { r ->
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
if (r.isNotOk) return@ActivityResultHandler
|
||||||
r.data?.handleGetContentResult(activity.contentResolver)?.pickAll()
|
r.data?.handleGetContentResult(activity.contentResolver)?.pickAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val arCamera = ActivityResultHandler(log) { r ->
|
private val prCamera = permissionSpecCamera.requester { openStillCamera() }
|
||||||
if (r.isNotOk) {
|
private val arCamera = ActivityResultHandler(log) { handleCameraResult(it) }
|
||||||
// 失敗したら DBからデータを削除
|
|
||||||
states.uriCameraImage?.let { uri ->
|
|
||||||
activity.contentResolver.delete(uri, null, null)
|
|
||||||
states.uriCameraImage = null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 画像のURL
|
|
||||||
when (val uri = r.data?.data ?: states.uriCameraImage) {
|
|
||||||
null -> activity.showToast(false, "missing image uri")
|
|
||||||
else -> callback.onPickAttachment(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val arCapture = ActivityResultHandler(log) { r ->
|
private val prCapture = permissionSpecCapture.requester { openPicker() }
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
private val arCapture = ActivityResultHandler(log) { handleCaptureResult(it) }
|
||||||
r.data?.data?.let { callback.onPickAttachment(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val arCustomThumbnail = ActivityResultHandler(log) { r ->
|
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
|
||||||
r.data
|
|
||||||
?.handleGetContentResult(activity.contentResolver)
|
|
||||||
?.firstOrNull()
|
|
||||||
?.let {
|
|
||||||
callback.resumeCustomThumbnailTarget(states.customThumbnailTargetId)?.let { pa ->
|
|
||||||
callback.onPickCustomThumbnail(pa, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val prPickAttachment = permissionSpecImagePicker.requester { openPicker() }
|
|
||||||
|
|
||||||
private val prPickCustomThumbnail = permissionSpecImagePicker.requester {
|
|
||||||
callback.resumeCustomThumbnailTarget(states.customThumbnailTargetId)
|
|
||||||
?.let { openCustomThumbnail(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// must register all ARHs before onStart
|
// must register all ARHs before onStart
|
||||||
prPickAttachment.register(activity)
|
prPickAudio.register(activity)
|
||||||
prPickCustomThumbnail.register(activity)
|
arPickAudio.register(activity)
|
||||||
arAttachmentChooser.register(activity)
|
|
||||||
|
prCamera.register(activity)
|
||||||
arCamera.register(activity)
|
arCamera.register(activity)
|
||||||
|
|
||||||
arCapture.register(activity)
|
arCapture.register(activity)
|
||||||
arCustomThumbnail.register(activity)
|
|
||||||
|
pickVisualMediaLauncher = activity.registerForActivityResult(
|
||||||
|
ActivityResultContracts.PickMultipleVisualMedia(4),
|
||||||
|
pickVisualMediaCallback,
|
||||||
|
)
|
||||||
|
pickThumbnailLauncher = activity.registerForActivityResult(
|
||||||
|
ActivityResultContracts.PickVisualMedia(),
|
||||||
|
pickThumbnailCallback,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -132,21 +124,17 @@ class AttachmentPicker(
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
fun openPicker() {
|
fun openPicker() {
|
||||||
if (!prPickAttachment.checkOrLaunch()) return
|
|
||||||
activity.run {
|
activity.run {
|
||||||
launchAndShowError {
|
launchAndShowError {
|
||||||
actionsDialog {
|
actionsDialog {
|
||||||
action(getString(R.string.pick_images)) {
|
action(getString(R.string.pick_images_or_video)) {
|
||||||
openAttachmentChooser(R.string.pick_images, "image/*", "video/*")
|
openVisualMediaPicker()
|
||||||
}
|
|
||||||
action(getString(R.string.pick_videos)) {
|
|
||||||
openAttachmentChooser(R.string.pick_videos, "video/*")
|
|
||||||
}
|
}
|
||||||
action(getString(R.string.pick_audios)) {
|
action(getString(R.string.pick_audios)) {
|
||||||
openAttachmentChooser(R.string.pick_audios, "audio/*")
|
openAudioPicker()
|
||||||
}
|
}
|
||||||
action(getString(R.string.image_capture)) {
|
action(getString(R.string.image_capture)) {
|
||||||
performCamera()
|
openStillCamera()
|
||||||
}
|
}
|
||||||
action(getString(R.string.video_capture)) {
|
action(getString(R.string.video_capture)) {
|
||||||
performCapture(
|
performCapture(
|
||||||
|
@ -165,43 +153,74 @@ class AttachmentPicker(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAttachmentChooser(titleId: Int, vararg mimeTypes: String) {
|
private fun openVisualMediaPicker() {
|
||||||
// SAFのIntentで開く
|
(pickVisualMediaLauncher
|
||||||
try {
|
?: error("pickVisualMediaLauncher is not registered."))
|
||||||
val intent = intentGetContent(true, activity.getString(titleId), mimeTypes)
|
.launch(
|
||||||
arAttachmentChooser.launch(intent)
|
PickVisualMediaRequest(
|
||||||
} catch (ex: Throwable) {
|
ActivityResultContracts.PickVisualMedia.ImageAndVideo
|
||||||
log.e(ex, "openAttachmentChooser failed.")
|
)
|
||||||
activity.showToast(ex, "openAttachmentChooser failed.")
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openAudioPicker() {
|
||||||
|
if (!prPickAudio.checkOrLaunch()) return
|
||||||
|
activity.launchAndShowError {
|
||||||
|
val intent = intentGetContent(
|
||||||
|
allowMultiple = true,
|
||||||
|
caption = activity.getString(R.string.pick_audios),
|
||||||
|
mimeTypes = arrayOf("audio/*"),
|
||||||
|
)
|
||||||
|
arPickAudio.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performCamera() {
|
private fun openStillCamera() {
|
||||||
try {
|
if (!prCamera.checkOrLaunch()) return
|
||||||
val values = ContentValues().apply {
|
activity.launchAndShowError {
|
||||||
put(MediaStore.Images.Media.TITLE, "${System.currentTimeMillis()}.jpg")
|
val newUri = activity.contentResolver.insert(
|
||||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
/* url = */
|
||||||
}
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
/* values = */
|
||||||
val newUri =
|
ContentValues().apply {
|
||||||
activity.contentResolver.insert(
|
put(MediaStore.Images.Media.TITLE, "${System.currentTimeMillis()}.jpg")
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||||
values
|
},
|
||||||
)
|
).also { states.uriCameraImage = it }
|
||||||
.also { states.uriCameraImage = it }
|
|
||||||
|
|
||||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||||
putExtra(MediaStore.EXTRA_OUTPUT, newUri)
|
putExtra(MediaStore.EXTRA_OUTPUT, newUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
arCamera.launch(intent)
|
arCamera.launch(intent)
|
||||||
} catch (ex: Throwable) {
|
|
||||||
log.e(ex, "performCamera failed.")
|
|
||||||
activity.showToast(ex, "performCamera failed.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performCapture(action: String, errorCaption: String) {
|
private fun handleCameraResult(r: ActivityResult) {
|
||||||
|
activity.launchAndShowError {
|
||||||
|
when {
|
||||||
|
r.isOk -> when (val uri = r.data?.data ?: states.uriCameraImage) {
|
||||||
|
null -> activity.showToast(false, "missing image uri")
|
||||||
|
else -> callback.onPickAttachment(uri)
|
||||||
|
}
|
||||||
|
// 失敗したら DBからデータを削除
|
||||||
|
else -> states.uriCameraImage?.let { uri ->
|
||||||
|
activity.contentResolver.delete(uri, null, null)
|
||||||
|
states.uriCameraImage = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 動画や音声をキャプチャする
|
||||||
|
* - Uriは呼び出し先に任せっきり
|
||||||
|
*/
|
||||||
|
private fun performCapture(
|
||||||
|
action: String,
|
||||||
|
errorCaption: String,
|
||||||
|
) {
|
||||||
|
if (!prCapture.checkOrLaunch()) return
|
||||||
try {
|
try {
|
||||||
arCapture.launch(Intent(action))
|
arCapture.launch(Intent(action))
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
|
@ -210,28 +229,46 @@ class AttachmentPicker(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ArrayList<GetContentResultEntry>.pickAll() =
|
private fun handleCaptureResult(r: ActivityResult) {
|
||||||
forEach { callback.onPickAttachment(it.uri, it.mimeType) }
|
activity.launchAndShowError {
|
||||||
|
if (r.isOk) {
|
||||||
|
when (val uri = r.data?.data) {
|
||||||
|
null -> activity.showToast(false, "missing media uri")
|
||||||
|
else -> callback.onPickAttachment(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<GetContentResultEntry>.pickAll() {
|
||||||
|
activity.launchAndShowError {
|
||||||
|
forEach { callback.onPickAttachment(it.uri, it.mimeType) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
// Mastodon's custom thumbnail
|
// Mastodon's custom thumbnail
|
||||||
|
|
||||||
fun openCustomThumbnail(pa: PostAttachment) {
|
fun openCustomThumbnail(attachmentId: String?) {
|
||||||
try {
|
states.customThumbnailTargetId = attachmentId
|
||||||
states.customThumbnailTargetId = pa.attachment?.id?.toString()
|
?: error("attachmentId is null")
|
||||||
?: return
|
activity.launchAndShowError {
|
||||||
if (!prPickCustomThumbnail.checkOrLaunch()) return
|
(pickThumbnailLauncher
|
||||||
// SAFのIntentで開く
|
?: error("pickThumbnailLauncher is not registered."))
|
||||||
arCustomThumbnail.launch(
|
.launch(
|
||||||
intentGetContent(
|
PickVisualMediaRequest(
|
||||||
false,
|
ActivityResultContracts.PickVisualMedia.ImageOnly,
|
||||||
activity.getString(R.string.pick_images),
|
)
|
||||||
arrayOf("image/*")
|
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
} catch (ex: Throwable) {
|
}
|
||||||
log.e(ex, "openCustomThumbnail failed.")
|
|
||||||
activity.showToast(ex, "openCustomThumbnail failed.")
|
private fun handleThumbnailResult(uri: Uri?) {
|
||||||
|
uri ?: return
|
||||||
|
activity.launchAndShowError {
|
||||||
|
listOf(uri).handleGetContentResult(activity.contentResolver).firstOrNull()?.let {
|
||||||
|
callback.onPickCustomThumbnail(states.customThumbnailTargetId, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,50 +43,111 @@ class PermissionSpec(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val permissionSpecNotification = if (Build.VERSION.SDK_INT >= 33) {
|
val permissionSpecNotification = when {
|
||||||
PermissionSpec(
|
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||||
permissions = listOf(
|
permissions = listOf(
|
||||||
Manifest.permission.POST_NOTIFICATIONS,
|
Manifest.permission.POST_NOTIFICATIONS,
|
||||||
),
|
),
|
||||||
deniedId = R.string.permission_denied_notifications,
|
deniedId = R.string.permission_denied_notifications,
|
||||||
rationalId = R.string.permission_rational_notifications,
|
rationalId = R.string.permission_rational_notifications,
|
||||||
)
|
)
|
||||||
} else {
|
else -> PermissionSpec(
|
||||||
PermissionSpec(
|
|
||||||
permissions = emptyList(),
|
permissions = emptyList(),
|
||||||
deniedId = R.string.permission_denied_notifications,
|
deniedId = R.string.permission_denied_notifications,
|
||||||
rationalId = R.string.permission_rational_notifications,
|
rationalId = R.string.permission_rational_notifications,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val permissionSpecMediaDownload = if (Build.VERSION.SDK_INT >= 33) {
|
val permissionSpecMediaDownload = when {
|
||||||
PermissionSpec(
|
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||||
permissions = listOf(Manifest.permission.POST_NOTIFICATIONS),
|
permissions = listOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
deniedId = R.string.permission_denied_download_manager,
|
deniedId = R.string.permission_denied_download_manager,
|
||||||
rationalId = R.string.permission_rational_download_manager,
|
rationalId = R.string.permission_rational_download_manager,
|
||||||
)
|
)
|
||||||
} else {
|
else -> PermissionSpec(
|
||||||
PermissionSpec(
|
|
||||||
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||||
deniedId = R.string.permission_denied_media_access,
|
deniedId = R.string.permission_denied_media_access,
|
||||||
rationalId = R.string.permission_rational_media_access,
|
rationalId = R.string.permission_rational_media_access,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val permissionSpecImagePicker = if (Build.VERSION.SDK_INT >= 33) {
|
/**
|
||||||
PermissionSpec(
|
* 静止画のみを端末から選ぶ
|
||||||
|
*/
|
||||||
|
val permissionSpecImagePicker = when {
|
||||||
|
Build.VERSION.SDK_INT >= 34 -> PermissionSpec(
|
||||||
|
permissions = emptyList(),
|
||||||
|
deniedId = R.string.permission_denied_media_access,
|
||||||
|
rationalId = R.string.permission_rational_media_access,
|
||||||
|
)
|
||||||
|
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||||
permissions = listOf(
|
permissions = listOf(
|
||||||
Manifest.permission.READ_MEDIA_IMAGES,
|
Manifest.permission.READ_MEDIA_IMAGES,
|
||||||
Manifest.permission.READ_MEDIA_VIDEO,
|
|
||||||
Manifest.permission.READ_MEDIA_AUDIO,
|
|
||||||
),
|
),
|
||||||
deniedId = R.string.permission_denied_media_access,
|
deniedId = R.string.permission_denied_media_access,
|
||||||
rationalId = R.string.permission_rational_media_access,
|
rationalId = R.string.permission_rational_media_access,
|
||||||
)
|
)
|
||||||
} else {
|
else -> PermissionSpec(
|
||||||
PermissionSpec(
|
|
||||||
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||||
deniedId = R.string.permission_denied_media_access,
|
deniedId = R.string.permission_denied_media_access,
|
||||||
rationalId = R.string.permission_rational_media_access,
|
rationalId = R.string.permission_rational_media_access,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* オーディオのみを端末から選ぶ
|
||||||
|
*/
|
||||||
|
val permissionSpecAudioPicker = when {
|
||||||
|
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||||
|
permissions = listOf(
|
||||||
|
Manifest.permission.READ_MEDIA_AUDIO,
|
||||||
|
),
|
||||||
|
deniedId = R.string.permission_denied_media_access,
|
||||||
|
rationalId = R.string.permission_rational_media_access,
|
||||||
|
)
|
||||||
|
else -> {
|
||||||
|
PermissionSpec(
|
||||||
|
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||||
|
deniedId = R.string.permission_denied_media_access,
|
||||||
|
rationalId = R.string.permission_rational_media_access,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* カメラ撮影用の実行時パーミッション
|
||||||
|
* https://developer.android.com/media/camera/camera-deprecated/photobasics?hl=ja#TaskPhotoView
|
||||||
|
*/
|
||||||
|
val permissionSpecCamera = when {
|
||||||
|
Build.VERSION.SDK_INT >= 29 -> PermissionSpec(
|
||||||
|
permissions = emptyList(),
|
||||||
|
deniedId = R.string.permission_denied_media_access,
|
||||||
|
rationalId = R.string.permission_rational_media_access,
|
||||||
|
)
|
||||||
|
else -> PermissionSpec(
|
||||||
|
permissions = listOf(
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
|
),
|
||||||
|
deniedId = R.string.permission_denied_media_access,
|
||||||
|
rationalId = R.string.permission_rational_media_access,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 動画や音声のキャプチャ前
|
||||||
|
*/
|
||||||
|
val permissionSpecCapture = when {
|
||||||
|
Build.VERSION.SDK_INT >= 29 -> PermissionSpec(
|
||||||
|
permissions = emptyList(),
|
||||||
|
deniedId = R.string.permission_denied_media_access,
|
||||||
|
rationalId = R.string.permission_rational_media_access,
|
||||||
|
)
|
||||||
|
else -> PermissionSpec(
|
||||||
|
permissions = listOf(
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
|
),
|
||||||
|
deniedId = R.string.permission_denied_media_access,
|
||||||
|
rationalId = R.string.permission_rational_media_access,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="64dp"/>
|
||||||
|
<stroke android:color="#08f" android:width="10dp"/>
|
||||||
|
</shape>
|
|
@ -30,8 +30,8 @@
|
||||||
tools:ignore="SmallSp"/>
|
tools:ignore="SmallSp"/>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="240dp"
|
android:layout_width="280dp"
|
||||||
android:layout_height="240dp"
|
android:layout_height="280dp"
|
||||||
android:layout_margin="10dp"
|
android:layout_margin="10dp"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
android:id="@+id/ivQrCode"
|
android:id="@+id/ivQrCode"
|
||||||
|
|
|
@ -537,6 +537,7 @@
|
||||||
<string name="performance">性能</string>
|
<string name="performance">性能</string>
|
||||||
<string name="pick_image">画像を選択</string>
|
<string name="pick_image">画像を選択</string>
|
||||||
<string name="pick_images">画像を選択…</string>
|
<string name="pick_images">画像を選択…</string>
|
||||||
|
<string name="pick_images_or_video">画像や動画を選択…</string>
|
||||||
<string name="pick_videos">動画を選択…</string>
|
<string name="pick_videos">動画を選択…</string>
|
||||||
<string name="pick_audios">音声ファイルを選択…</string>
|
<string name="pick_audios">音声ファイルを選択…</string>
|
||||||
<string name="please_add_account">アカウントがありません。事前にアカウントの追加を行ってください。</string>
|
<string name="please_add_account">アカウントがありません。事前にアカウントの追加を行ってください。</string>
|
||||||
|
|
|
@ -320,6 +320,7 @@
|
||||||
<string name="color_and_background">Color and Background…</string>
|
<string name="color_and_background">Color and Background…</string>
|
||||||
<string name="column_background">Column background</string>
|
<string name="column_background">Column background</string>
|
||||||
<string name="pick_image">Pick an image</string>
|
<string name="pick_image">Pick an image</string>
|
||||||
|
<string name="pick_images_or_video">Pick images or video…</string>
|
||||||
<string name="pick_images">Pick image(s)…</string>
|
<string name="pick_images">Pick image(s)…</string>
|
||||||
<string name="pick_videos">Pick video(s)…</string>
|
<string name="pick_videos">Pick video(s)…</string>
|
||||||
<string name="pick_audios">Choose audio file(s)…</string>
|
<string name="pick_audios">Choose audio file(s)…</string>
|
||||||
|
|
|
@ -77,7 +77,7 @@ dependencies {
|
||||||
api("androidx.emoji2:emoji2-views-helper:${Vers.emoji2Version}")
|
api("androidx.emoji2:emoji2-views-helper:${Vers.emoji2Version}")
|
||||||
api("androidx.emoji2:emoji2-views:${Vers.emoji2Version}")
|
api("androidx.emoji2:emoji2-views:${Vers.emoji2Version}")
|
||||||
api("androidx.emoji2:emoji2:${Vers.emoji2Version}")
|
api("androidx.emoji2:emoji2:${Vers.emoji2Version}")
|
||||||
api("androidx.exifinterface:exifinterface:1.3.6")
|
api("androidx.exifinterface:exifinterface:1.3.7")
|
||||||
api("androidx.lifecycle:lifecycle-common-java8:${Vers.lifecycleVersion}")
|
api("androidx.lifecycle:lifecycle-common-java8:${Vers.lifecycleVersion}")
|
||||||
api("androidx.lifecycle:lifecycle-livedata-ktx:${Vers.lifecycleVersion}")
|
api("androidx.lifecycle:lifecycle-livedata-ktx:${Vers.lifecycleVersion}")
|
||||||
api("androidx.lifecycle:lifecycle-process:${Vers.lifecycleVersion}")
|
api("androidx.lifecycle:lifecycle-process:${Vers.lifecycleVersion}")
|
||||||
|
@ -100,14 +100,12 @@ dependencies {
|
||||||
api("com.otaliastudios:transcoder:0.10.5")
|
api("com.otaliastudios:transcoder:0.10.5")
|
||||||
api("com.squareup.okhttp3:okhttp-urlconnection:${Vers.okhttpVersion}")
|
api("com.squareup.okhttp3:okhttp-urlconnection:${Vers.okhttpVersion}")
|
||||||
api("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
|
api("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
|
||||||
// api( "io.github.inflationx:calligraphy3:3.1.1")
|
|
||||||
// api( "io.github.inflationx:viewpump:2.1.1")
|
|
||||||
api("org.bouncycastle:bcprov-jdk15on:1.70")
|
api("org.bouncycastle:bcprov-jdk15on:1.70")
|
||||||
api("org.jetbrains.kotlin:kotlin-reflect:${Vers.kotlinVersion}")
|
api("org.jetbrains.kotlin:kotlin-reflect:${Vers.kotlinVersion}")
|
||||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Vers.kotlinxCoroutinesVersion}")
|
api("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Vers.kotlinxCoroutinesVersion}")
|
||||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Vers.kotlinxCoroutinesVersion}")
|
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Vers.kotlinxCoroutinesVersion}")
|
||||||
api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
|
api("org.jetbrains.kotlinx:kotlinx-serialization-json:${Vers.kotlinxSerializationLibVersion}")
|
||||||
api("ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0")
|
api("ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0")
|
||||||
|
|
||||||
//non-OSS dependency api "androidx.media3:media3-cast:$media3Version"
|
//non-OSS dependency api "androidx.media3:media3-cast:$media3Version"
|
||||||
|
|
|
@ -96,11 +96,7 @@ suspend fun transcodeAudio(
|
||||||
}
|
}
|
||||||
val transformer = Transformer.Builder(context)
|
val transformer = Transformer.Builder(context)
|
||||||
.setLooper(looper)
|
.setLooper(looper)
|
||||||
.setTransformationRequest(
|
.setAudioMimeType(encodeMimeType)
|
||||||
TransformationRequest.Builder()
|
|
||||||
.setAudioMimeType(encodeMimeType)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.addListener(transformerListener)
|
.addListener(transformerListener)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|
|
@ -153,23 +153,28 @@ fun AppCompatActivity.overrideActivityTransitionCompat(
|
||||||
@AnimRes animEnter: Int,
|
@AnimRes animEnter: Int,
|
||||||
@AnimRes animExit: Int,
|
@AnimRes animExit: Int,
|
||||||
) {
|
) {
|
||||||
if (Build.VERSION.SDK_INT >= 34) {
|
when {
|
||||||
overrideActivityTransition(
|
Build.VERSION.SDK_INT >= 34 -> {
|
||||||
when (overrideType) {
|
overrideActivityTransition(
|
||||||
TransitionOverrideType.Open ->
|
when (overrideType) {
|
||||||
AppCompatActivity.OVERRIDE_TRANSITION_OPEN
|
TransitionOverrideType.Open ->
|
||||||
|
AppCompatActivity.OVERRIDE_TRANSITION_OPEN
|
||||||
|
|
||||||
TransitionOverrideType.Close ->
|
TransitionOverrideType.Close ->
|
||||||
AppCompatActivity.OVERRIDE_TRANSITION_CLOSE
|
AppCompatActivity.OVERRIDE_TRANSITION_CLOSE
|
||||||
},
|
},
|
||||||
animEnter,
|
animEnter,
|
||||||
animExit
|
animExit
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
overridePendingTransition(
|
|
||||||
animEnter,
|
else -> {
|
||||||
animExit,
|
@Suppress("DEPRECATION")
|
||||||
)
|
overridePendingTransition(
|
||||||
|
animEnter,
|
||||||
|
animExit,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,30 @@ fun AppCompatActivity.launchAndShowError(
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
suspend fun <T:Any?> AppCompatActivity.withProgress(
|
||||||
|
caption:String,
|
||||||
|
progressInitializer: suspend (ProgressDialogEx) -> Unit = {},
|
||||||
|
block: suspend (progress :ProgressDialogEx)->T,
|
||||||
|
):T {
|
||||||
|
val activity = this
|
||||||
|
var progress: ProgressDialogEx? = null
|
||||||
|
try {
|
||||||
|
progress = ProgressDialogEx(activity)
|
||||||
|
progress.setCancelable(true)
|
||||||
|
progress.isIndeterminateEx = true
|
||||||
|
progress.setMessageEx(caption)
|
||||||
|
progressInitializer(progress)
|
||||||
|
progress.show()
|
||||||
|
return supervisorScope {
|
||||||
|
val task = async(AppDispatchers.MainImmediate) { block(progress) }
|
||||||
|
progress.setOnCancelListener { task.cancel() }
|
||||||
|
task.await()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
progress?.dismissSafe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun <T : Any?> AppCompatActivity.launchProgress(
|
fun <T : Any?> AppCompatActivity.launchProgress(
|
||||||
caption: String,
|
caption: String,
|
||||||
doInBackground: suspend CoroutineScope.(ProgressDialogEx) -> T,
|
doInBackground: suspend CoroutineScope.(ProgressDialogEx) -> T,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package jp.juggler.util.data
|
package jp.juggler.util.data
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -283,28 +284,26 @@ data class GetContentResultEntry(
|
||||||
var time: Long? = null,
|
var time: Long? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
// returns list of pair of uri and mime-type.
|
fun MutableList<GetContentResultEntry>.addNoDuplicate(
|
||||||
fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList<GetContentResultEntry> {
|
contentResolver: ContentResolver,
|
||||||
val urlList = ArrayList<GetContentResultEntry>()
|
uri: Uri?,
|
||||||
// 単一選択
|
type: String? = null,
|
||||||
data?.let {
|
) {
|
||||||
val mimeType = try {
|
uri ?: return
|
||||||
type ?: contentResolver.getType(it)
|
if (any { it.uri == uri }) return
|
||||||
} catch (ex: Throwable) {
|
val mimeType = try {
|
||||||
log.w(ex, "contentResolver.getType failed. uri=$it")
|
type ?: contentResolver.getType(uri)
|
||||||
null
|
} catch (ex: Throwable) {
|
||||||
}
|
log.w(ex, "contentResolver.getType failed. uri=$uri")
|
||||||
urlList.add(GetContentResultEntry(it, mimeType))
|
return
|
||||||
}
|
}
|
||||||
// 複数選択
|
add(GetContentResultEntry(uri, mimeType))
|
||||||
this.clipData?.let { clipData ->
|
}
|
||||||
for (uri in (0 until clipData.itemCount).mapNotNull { clipData.getItemAt(it)?.uri }) {
|
|
||||||
if (urlList.none { it.uri == uri }) {
|
fun List<GetContentResultEntry>.grantPermissions(
|
||||||
urlList.add(GetContentResultEntry(uri))
|
contentResolver: ContentResolver,
|
||||||
}
|
) {
|
||||||
}
|
forEach {
|
||||||
}
|
|
||||||
urlList.forEach {
|
|
||||||
try {
|
try {
|
||||||
contentResolver.takePersistableUriPermission(
|
contentResolver.takePersistableUriPermission(
|
||||||
it.uri,
|
it.uri,
|
||||||
|
@ -313,5 +312,29 @@ fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList<G
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return urlList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns list of pair of uri and mime-type.
|
||||||
|
fun List<Uri>.handleGetContentResult(contentResolver: ContentResolver) =
|
||||||
|
buildList {
|
||||||
|
this@handleGetContentResult.forEach {
|
||||||
|
addNoDuplicate(contentResolver, it)
|
||||||
|
}
|
||||||
|
grantPermissions(contentResolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ClipData.uris
|
||||||
|
get() = (0 until itemCount).mapNotNull { getItemAt(it)?.uri }
|
||||||
|
|
||||||
|
// returns list of pair of uri and mime-type.
|
||||||
|
fun Intent.handleGetContentResult(contentResolver: ContentResolver) =
|
||||||
|
buildList {
|
||||||
|
// 単一選択
|
||||||
|
addNoDuplicate(contentResolver, data, type)
|
||||||
|
|
||||||
|
// 複数選択
|
||||||
|
clipData?.uris?.forEach {
|
||||||
|
addNoDuplicate(contentResolver, it)
|
||||||
|
}
|
||||||
|
grantPermissions(contentResolver)
|
||||||
|
}
|
||||||
|
|
|
@ -289,18 +289,14 @@ fun createResizedBitmap(
|
||||||
// 出力用Bitmap作成
|
// 出力用Bitmap作成
|
||||||
val dst = Bitmap.createBitmap(dstSizeInt.x, dstSizeInt.y, Bitmap.Config.ARGB_8888)
|
val dst = Bitmap.createBitmap(dstSizeInt.x, dstSizeInt.y, Bitmap.Config.ARGB_8888)
|
||||||
try {
|
try {
|
||||||
if (dst == null) {
|
val canvas = Canvas(dst)
|
||||||
context.showToast(false, "bitmap creation failed.")
|
val paint = Paint()
|
||||||
} else {
|
paint.isFilterBitmap = true
|
||||||
val canvas = Canvas(dst)
|
canvas.drawBitmap(sourceBitmap, matrix, paint)
|
||||||
val paint = Paint()
|
log.d("createResizedBitmap: resized to ${dstSizeInt.x}x${dstSizeInt.y}")
|
||||||
paint.isFilterBitmap = true
|
return dst
|
||||||
canvas.drawBitmap(sourceBitmap, matrix, paint)
|
|
||||||
log.d("createResizedBitmap: resized to ${dstSizeInt.x}x${dstSizeInt.y}")
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
dst?.recycle()
|
dst.recycle()
|
||||||
throw ex
|
throw ex
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -12,7 +12,6 @@ import androidx.media3.transformer.Effects
|
||||||
import androidx.media3.transformer.ExportException
|
import androidx.media3.transformer.ExportException
|
||||||
import androidx.media3.transformer.ExportResult
|
import androidx.media3.transformer.ExportResult
|
||||||
import androidx.media3.transformer.ProgressHolder
|
import androidx.media3.transformer.ProgressHolder
|
||||||
import androidx.media3.transformer.TransformationRequest
|
|
||||||
import androidx.media3.transformer.Transformer
|
import androidx.media3.transformer.Transformer
|
||||||
import androidx.media3.transformer.VideoEncoderSettings
|
import androidx.media3.transformer.VideoEncoderSettings
|
||||||
import com.otaliastudios.transcoder.Transcoder
|
import com.otaliastudios.transcoder.Transcoder
|
||||||
|
@ -23,9 +22,14 @@ import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||||
import jp.juggler.util.coroutine.AppDispatchers
|
import jp.juggler.util.coroutine.AppDispatchers
|
||||||
import jp.juggler.util.data.clip
|
import jp.juggler.util.data.clip
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
@ -38,19 +42,19 @@ import kotlin.math.sqrt
|
||||||
|
|
||||||
private val log = LogCategory("MovieUtils")
|
private val log = LogCategory("MovieUtils")
|
||||||
|
|
||||||
enum class MovideResizeMode(val int: Int) {
|
enum class MovieResizeMode(val int: Int) {
|
||||||
Auto(0),
|
Auto(0),
|
||||||
No(1),
|
No(1),
|
||||||
Always(2),
|
Always(2),
|
||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(i: Int) = values().find { it.int == i } ?: Auto
|
fun fromInt(i: Int) = entries.find { it.int == i } ?: Auto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MovieResizeConfig(
|
data class MovieResizeConfig(
|
||||||
val mode: MovideResizeMode,
|
val mode: MovieResizeMode,
|
||||||
val limitFrameRate: Int,
|
val limitFrameRate: Int,
|
||||||
val limitBitrate: Long,
|
val limitBitrate: Long,
|
||||||
val limitSquarePixels: Int,
|
val limitSquarePixels: Int,
|
||||||
|
@ -78,9 +82,9 @@ data class MovieResizeConfig(
|
||||||
|
|
||||||
// トランスコードをスキップする判定
|
// トランスコードをスキップする判定
|
||||||
fun isTranscodeRequired(info: VideoInfo) = when (mode) {
|
fun isTranscodeRequired(info: VideoInfo) = when (mode) {
|
||||||
MovideResizeMode.No -> false
|
MovieResizeMode.No -> false
|
||||||
MovideResizeMode.Always -> true
|
MovieResizeMode.Always -> true
|
||||||
MovideResizeMode.Auto ->
|
MovieResizeMode.Auto ->
|
||||||
info.squarePixels > limitSquarePixels ||
|
info.squarePixels > limitSquarePixels ||
|
||||||
(info.actualBps ?: 0).toFloat() > limitBitrate.toFloat() * 1.5f ||
|
(info.actualBps ?: 0).toFloat() > limitBitrate.toFloat() * 1.5f ||
|
||||||
(info.frameRatio == null || info.frameRatio < 1f || info.frameRatio > limitFrameRate)
|
(info.frameRatio == null || info.frameRatio < 1f || info.frameRatio > limitFrameRate)
|
||||||
|
@ -129,9 +133,9 @@ suspend fun transcodeVideoMedia3Transformer(
|
||||||
withContext(AppDispatchers.MainImmediate) {
|
withContext(AppDispatchers.MainImmediate) {
|
||||||
|
|
||||||
when (resizeConfig.mode) {
|
when (resizeConfig.mode) {
|
||||||
MovideResizeMode.No -> return@withContext inFile
|
MovieResizeMode.No -> return@withContext inFile
|
||||||
MovideResizeMode.Always -> Unit
|
MovieResizeMode.Always -> Unit
|
||||||
MovideResizeMode.Auto -> {
|
MovieResizeMode.Auto -> {
|
||||||
if (!resizeConfig.isTranscodeRequired(info)) {
|
if (!resizeConfig.isTranscodeRequired(info)) {
|
||||||
log.i("transcodeVideoMedia3Transformer: transcode not required.")
|
log.i("transcodeVideoMedia3Transformer: transcode not required.")
|
||||||
return@withContext inFile
|
return@withContext inFile
|
||||||
|
@ -189,12 +193,6 @@ suspend fun transcodeVideoMedia3Transformer(
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val request = TransformationRequest.Builder().apply {
|
|
||||||
setVideoMimeType(MimeTypes.VIDEO_H264)
|
|
||||||
setAudioMimeType(MimeTypes.AUDIO_AAC)
|
|
||||||
// ビットレートがないな…
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
// 完了検知
|
// 完了検知
|
||||||
val completed = AtomicBoolean(false)
|
val completed = AtomicBoolean(false)
|
||||||
val error = AtomicReference<Throwable>(null)
|
val error = AtomicReference<Throwable>(null)
|
||||||
|
@ -226,9 +224,11 @@ suspend fun transcodeVideoMedia3Transformer(
|
||||||
// 開始
|
// 開始
|
||||||
val transformer = Transformer.Builder(context).apply {
|
val transformer = Transformer.Builder(context).apply {
|
||||||
setEncoderFactory(encoderFactory)
|
setEncoderFactory(encoderFactory)
|
||||||
setTransformationRequest(request)
|
setAudioMimeType(MimeTypes.AUDIO_AAC)
|
||||||
|
setVideoMimeType(MimeTypes.VIDEO_H264)
|
||||||
addListener(listener)
|
addListener(listener)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
transformer.start(editedMediaItem, outFile.canonicalPath)
|
transformer.start(editedMediaItem, outFile.canonicalPath)
|
||||||
|
|
||||||
// 完了まで待機しつつ、定期的に進捗コールバックを呼ぶ
|
// 完了まで待機しつつ、定期的に進捗コールバックを呼ぶ
|
||||||
|
@ -265,9 +265,9 @@ suspend fun transcodeVideo(
|
||||||
}
|
}
|
||||||
|
|
||||||
when (resizeConfig.mode) {
|
when (resizeConfig.mode) {
|
||||||
MovideResizeMode.No -> return@withContext inFile
|
MovieResizeMode.No -> return@withContext inFile
|
||||||
MovideResizeMode.Always -> Unit
|
MovieResizeMode.Always -> Unit
|
||||||
MovideResizeMode.Auto -> {
|
MovieResizeMode.Auto -> {
|
||||||
if (info.squarePixels <= resizeConfig.limitSquarePixels &&
|
if (info.squarePixels <= resizeConfig.limitSquarePixels &&
|
||||||
(info.actualBps ?: 0).toFloat() <= resizeConfig.limitBitrate * 1.5f &&
|
(info.actualBps ?: 0).toFloat() <= resizeConfig.limitBitrate * 1.5f &&
|
||||||
(info.frameRatio?.toInt() ?: 0) <= resizeConfig.limitFrameRate
|
(info.frameRatio?.toInt() ?: 0) <= resizeConfig.limitFrameRate
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package jp.juggler.util.os
|
package jp.juggler.util.os
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* インストゥルメントテストのContextは
|
* インストゥルメントテストのContextは
|
||||||
|
@ -18,3 +20,6 @@ val Context.applicationContextSafe: Context
|
||||||
|
|
||||||
fun Context.error(@StringRes resId: Int, vararg args: Any?): Nothing =
|
fun Context.error(@StringRes resId: Int, vararg args: Any?): Nothing =
|
||||||
error(getString(resId, *args))
|
error(getString(resId, *args))
|
||||||
|
|
||||||
|
fun Context.resDrawable(@DrawableRes resId: Int) =
|
||||||
|
ContextCompat.getDrawable(this, resId)
|
||||||
|
|
|
@ -378,6 +378,9 @@ val AppCompatActivity.isLiveActivity: Boolean
|
||||||
val ActivityResult.isNotOk
|
val ActivityResult.isNotOk
|
||||||
get() = Activity.RESULT_OK != resultCode
|
get() = Activity.RESULT_OK != resultCode
|
||||||
|
|
||||||
|
val ActivityResult.isOk
|
||||||
|
get() = Activity.RESULT_OK == resultCode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ringtone pickerの処理結果のUriまたはnull
|
* Ringtone pickerの処理結果のUriまたはnull
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -21,7 +21,8 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.jetbrains.kotlin.jvm") version (Vers.kotlinVersion) apply false
|
kotlin("jvm") version (Vers.kotlinVersion) apply false
|
||||||
|
kotlin("plugin.serialization") version (Vers.kotlinxSerializationPluginVersion) apply true // !!
|
||||||
id("org.jetbrains.kotlin.android") version (Vers.kotlinVersion) apply false
|
id("org.jetbrains.kotlin.android") version (Vers.kotlinVersion) apply false
|
||||||
id("com.google.devtools.ksp") version (Vers.kspVersion) apply false
|
id("com.google.devtools.ksp") version (Vers.kspVersion) apply false
|
||||||
}
|
}
|
||||||
|
@ -31,7 +32,7 @@ allprojects {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
||||||
// com.github.androidmads:QRGenerator
|
// alexzhirkevich/custom-qr-generator
|
||||||
maven(url = "https://jitpack.io")
|
maven(url = "https://jitpack.io")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,51 @@
|
||||||
import org.gradle.api.JavaVersion
|
import org.gradle.api.JavaVersion
|
||||||
|
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
object Vers {
|
object Vers {
|
||||||
|
const val stBuildToolsVersion = "34.0.0"
|
||||||
|
const val stCompileSdkVersion = 34
|
||||||
|
const val stTargetSdkVersion = 34
|
||||||
|
const val stMinSdkVersion = 26
|
||||||
|
|
||||||
val javaSourceCompatibility = JavaVersion.VERSION_1_8
|
val javaSourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
val javaTargetCompatibility = JavaVersion.VERSION_1_8
|
val javaTargetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
|
||||||
const val androidGradlePruginVersion = "8.1.4"
|
const val kotlinVersion = "1.9.22"
|
||||||
|
const val kotlinJvmTarget = "1.8"
|
||||||
|
const val kotlinJvmToolchain = 17
|
||||||
|
|
||||||
|
const val androidGradlePruginVersion = "8.2.1"
|
||||||
|
|
||||||
const val androidxAnnotationVersion = "1.6.0"
|
const val androidxAnnotationVersion = "1.6.0"
|
||||||
const val androidxTestEspressoCoreVersion = "3.5.1"
|
const val androidxTestEspressoCoreVersion = "3.5.1"
|
||||||
const val androidxTestExtJunitVersion = "1.1.5"
|
const val androidxTestExtJunitVersion = "1.1.5"
|
||||||
const val androidxTestVersion = "1.5.0"
|
const val androidxTestVersion = "1.5.0"
|
||||||
const val ankoVersion = "0.10.8"
|
|
||||||
|
// const val ankoVersion = "0.10.8"
|
||||||
const val appcompatVersion = "1.6.1"
|
const val appcompatVersion = "1.6.1"
|
||||||
const val archVersion = "2.2.0"
|
const val archVersion = "2.2.0"
|
||||||
const val commonsCodecVersion = "1.16.0"
|
const val commonsCodecVersion = "1.16.0"
|
||||||
const val composeVersion = "1.0.5"
|
const val composeVersion = "1.0.5"
|
||||||
const val conscryptVersion = "2.5.2"
|
const val conscryptVersion = "2.5.2"
|
||||||
const val coreKtxVersion = "1.12.0"
|
const val coreKtxVersion = "1.12.0"
|
||||||
const val desugarLibVersion = "2.0.3"
|
const val desugarLibVersion = "2.0.4"
|
||||||
const val detektVersion = "1.23.4"
|
const val detektVersion = "1.23.4"
|
||||||
const val emoji2Version = "1.4.0"
|
const val emoji2Version = "1.4.0"
|
||||||
const val glideVersion = "4.15.1"
|
const val glideVersion = "4.15.1"
|
||||||
const val junitVersion = "4.13.2"
|
const val junitVersion = "4.13.2"
|
||||||
const val koinVersion = "3.5.0"
|
const val koinVersion = "3.5.0"
|
||||||
const val kotlinJvmTarget = "1.8"
|
const val kotlinTestVersion = kotlinVersion // "1.9.22"
|
||||||
const val kotlinJvmToolchain = 17
|
|
||||||
const val kotlinTestVersion = "1.9.20"
|
|
||||||
const val kotlinVersion = "1.9.20"
|
|
||||||
const val kotlinxCoroutinesVersion = "1.7.3"
|
const val kotlinxCoroutinesVersion = "1.7.3"
|
||||||
const val kspVersion = "1.9.20-1.0.14"
|
const val kotlinxSerializationPluginVersion = kotlinVersion
|
||||||
|
const val kotlinxSerializationLibVersion = "1.6.2"
|
||||||
|
const val kspVersion = "$kotlinVersion-1.0.16"
|
||||||
const val lifecycleVersion = "2.6.2"
|
const val lifecycleVersion = "2.6.2"
|
||||||
const val materialVersion = "1.10.0"
|
const val materialVersion = "1.11.0"
|
||||||
const val media3Version = "1.2.0"
|
const val media3Version = "1.2.0"
|
||||||
const val okhttpVersion = "5.0.0-alpha.11"
|
const val okhttpVersion = "5.0.0-alpha.11"
|
||||||
const val preferenceKtxVersion = "1.2.1"
|
const val preferenceKtxVersion = "1.2.1"
|
||||||
const val stBuildToolsVersion = "34.0.0"
|
|
||||||
const val stCompileSdkVersion = 34
|
|
||||||
const val stMinSdkVersion = 26
|
|
||||||
const val stTargetSdkVersion = 34
|
|
||||||
const val startupVersion = "1.1.1"
|
const val startupVersion = "1.1.1"
|
||||||
const val testKtxVersion = "1.5.0"
|
const val testKtxVersion = "1.5.0"
|
||||||
const val webpDecoderVersion = "2.6.$glideVersion"
|
const val webpDecoderVersion = "2.6.$glideVersion"
|
||||||
const val workVersion = "2.8.1"
|
const val workVersion = "2.9.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,19 +27,19 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility =Vers. javaSourceCompatibility
|
sourceCompatibility = Vers.javaSourceCompatibility
|
||||||
targetCompatibility =Vers.javaTargetCompatibility
|
targetCompatibility = Vers.javaTargetCompatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(Vers.kotlinJvmToolchain)
|
jvmToolchain(Vers.kotlinJvmToolchain)
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget =Vers. kotlinJvmTarget
|
jvmTarget = Vers.kotlinJvmTarget
|
||||||
}
|
}
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget =Vers. kotlinJvmTarget
|
jvmTarget = Vers.kotlinJvmTarget
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#Mon Jun 13 20:53:58 JST 2022
|
#Mon Jun 13 20:53:58 JST 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -40,6 +40,7 @@ android {
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = Vers.javaSourceCompatibility
|
sourceCompatibility = Vers.javaSourceCompatibility
|
||||||
targetCompatibility = Vers.javaTargetCompatibility
|
targetCompatibility = Vers.javaTargetCompatibility
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
|
|
Loading…
Reference in New Issue