アカウント設定に投稿する動画を再圧縮する設定を追加
This commit is contained in:
parent
b0ddddfe49
commit
57f026518a
|
@ -296,6 +296,9 @@ dependencies {
|
|||
implementation "io.insert-koin:koin-androidx-workmanager:$koin_version"
|
||||
// implementation "io.insert-koin:koin-androidx-navigation:$koin_version"
|
||||
// implementation "io.insert-koin:koin-androidx-compose:$koin_version"
|
||||
|
||||
// video transcoder https://github.com/natario1/Transcoder
|
||||
implementation "com.otaliastudios:transcoder:0.10.4"
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
|
|
@ -76,6 +76,29 @@ class ActAccountSetting : AppCompatActivity(), View.OnClickListener,
|
|||
Intent(activity, ActAccountSetting::class.java).apply {
|
||||
putExtra(KEY_ACCOUNT_DB_ID, ai.db_id)
|
||||
}
|
||||
|
||||
fun simpleTextWatcher(block: () -> Unit) = object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
block()
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
before: Int,
|
||||
count: Int,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
|
@ -268,6 +291,18 @@ class ActAccountSetting : AppCompatActivity(), View.OnClickListener,
|
|||
setDropDownViewResource(R.layout.lv_spinner_dropdown)
|
||||
}
|
||||
|
||||
spMovieTranscodeMode.adapter = ArrayAdapter(
|
||||
this@ActAccountSetting,
|
||||
android.R.layout.simple_spinner_item,
|
||||
arrayOf(
|
||||
getString(R.string.auto),
|
||||
getString(R.string.no),
|
||||
getString(R.string.always),
|
||||
)
|
||||
).apply {
|
||||
setDropDownViewResource(R.layout.lv_spinner_dropdown)
|
||||
}
|
||||
|
||||
pushPolicyItems = listOf(
|
||||
PushPolicyItem(null, getString(R.string.unspecified)),
|
||||
PushPolicyItem("all", getString(R.string.all)),
|
||||
|
@ -312,52 +347,26 @@ class ActAccountSetting : AppCompatActivity(), View.OnClickListener,
|
|||
NetworkEmojiInvalidator(handler, it)
|
||||
}
|
||||
|
||||
etDefaultText.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
saveUIToData()
|
||||
}
|
||||
val watcher1 = simpleTextWatcher { saveUIToData() }
|
||||
arrayOf(
|
||||
etDefaultText,
|
||||
etMediaSizeMax,
|
||||
etMovieSizeMax,
|
||||
etMovieBitrate,
|
||||
etMovieFrameRate,
|
||||
etMovieSquarePixels,
|
||||
).forEach {
|
||||
it.addTextChangedListener(watcher1)
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
before: Int,
|
||||
count: Int,
|
||||
) {
|
||||
}
|
||||
})
|
||||
|
||||
etMaxTootChars.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
before: Int,
|
||||
count: Int,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
etMaxTootChars.addTextChangedListener(
|
||||
simpleTextWatcher{
|
||||
val num = etMaxTootChars.parseInt()
|
||||
if (num != null && num >= 0) {
|
||||
saveUIToData()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
arrayOf(
|
||||
btnOpenBrowser,
|
||||
|
@ -409,6 +418,7 @@ class ActAccountSetting : AppCompatActivity(), View.OnClickListener,
|
|||
arrayOf(
|
||||
spResizeImage,
|
||||
spPushPolicy,
|
||||
spMovieTranscodeMode,
|
||||
).forEach { it.onItemSelectedListener = this@ActAccountSetting }
|
||||
}
|
||||
}
|
||||
|
@ -456,6 +466,40 @@ class ActAccountSetting : AppCompatActivity(), View.OnClickListener,
|
|||
etDefaultText.setText(a.default_text)
|
||||
etMaxTootChars.setText(a.max_toot_chars.toString())
|
||||
|
||||
val ti = TootInstance.getCached(a)
|
||||
if (ti == null) {
|
||||
etMediaSizeMax.setText(a.image_max_megabytes ?: "")
|
||||
etMovieSizeMax.setText(a.movie_max_megabytes ?: "")
|
||||
} else {
|
||||
etMediaSizeMax.setText(
|
||||
a.image_max_megabytes
|
||||
?: a.getImageMaxBytes(ti).div(1000000).toString()
|
||||
)
|
||||
etMovieSizeMax.setText(
|
||||
a.movie_max_megabytes
|
||||
?: a.getMovieMaxBytes(ti).div(1000000).toString()
|
||||
)
|
||||
}
|
||||
|
||||
val currentResizeConfig = a.getResizeConfig()
|
||||
var index =
|
||||
imageResizeItems.indexOfFirst { it.config.spec == currentResizeConfig.spec }
|
||||
log.d("ResizeItem current ${currentResizeConfig.spec} index=$index ")
|
||||
if (index == -1) index =
|
||||
imageResizeItems.indexOfFirst { it.config.spec == SavedAccount.defaultResizeConfig.spec }
|
||||
spResizeImage.setSelection(index, false)
|
||||
|
||||
val currentPushPolicy = a.push_policy
|
||||
index = pushPolicyItems.indexOfFirst { it.id == currentPushPolicy }
|
||||
if (index == -1) index = 0
|
||||
spPushPolicy.setSelection(index, false)
|
||||
|
||||
spMovieTranscodeMode.setSelection(max(0, a.movieTranscodeMode), false)
|
||||
etMovieFrameRate.setText(a.movieTranscodeFramerate)
|
||||
etMovieBitrate.setText(a.movieTranscodeBitrate)
|
||||
etMovieSquarePixels.setText(a.movieTranscodeSquarePixels)
|
||||
|
||||
// アカウントからUIへのデータロードはここまで
|
||||
loadingBusy = false
|
||||
|
||||
val enabled = !a.isPseudo
|
||||
|
@ -497,34 +541,6 @@ class ActAccountSetting : AppCompatActivity(), View.OnClickListener,
|
|||
btnNotificationStyleEdit,
|
||||
btnNotificationStyleEditReply,
|
||||
).forEach { it.isEnabledAlpha = enabledNewNotification }
|
||||
|
||||
val ti = TootInstance.getCached(a)
|
||||
if (ti == null) {
|
||||
etMediaSizeMax.setText(a.image_max_megabytes ?: "")
|
||||
etMovieSizeMax.setText(a.movie_max_megabytes ?: "")
|
||||
} else {
|
||||
etMediaSizeMax.setText(
|
||||
a.image_max_megabytes
|
||||
?: a.getImageMaxBytes(ti).div(1000000).toString()
|
||||
)
|
||||
etMovieSizeMax.setText(
|
||||
a.movie_max_megabytes
|
||||
?: a.getMovieMaxBytes(ti).div(1000000).toString()
|
||||
)
|
||||
}
|
||||
|
||||
val currentResizeConfig = a.getResizeConfig()
|
||||
var index =
|
||||
imageResizeItems.indexOfFirst { it.config.spec == currentResizeConfig.spec }
|
||||
log.d("ResizeItem current ${currentResizeConfig.spec} index=$index ")
|
||||
if (index == -1) index =
|
||||
imageResizeItems.indexOfFirst { it.config.spec == SavedAccount.defaultResizeConfig.spec }
|
||||
spResizeImage.setSelection(index, false)
|
||||
|
||||
val currentPushPolicy = a.push_policy
|
||||
index = pushPolicyItems.indexOfFirst { it.id == currentPushPolicy }
|
||||
if (index == -1) index = 0
|
||||
spPushPolicy.setSelection(index, false)
|
||||
}
|
||||
|
||||
showVisibility()
|
||||
|
@ -595,8 +611,12 @@ class ActAccountSetting : AppCompatActivity(), View.OnClickListener,
|
|||
|
||||
account.push_policy =
|
||||
pushPolicyItems.elementAtOrNull(spPushPolicy.selectedItemPosition)?.id
|
||||
}
|
||||
|
||||
account.movieTranscodeMode = spMovieTranscodeMode.selectedItemPosition
|
||||
account.movieTranscodeBitrate = etMovieBitrate.text.toString()
|
||||
account.movieTranscodeFramerate = etMovieFrameRate.text.toString()
|
||||
account.movieTranscodeSquarePixels = etMovieSquarePixels.text.toString()
|
||||
}
|
||||
|
||||
account.saveSetting()
|
||||
}
|
||||
|
@ -1439,7 +1459,7 @@ class ActAccountSetting : AppCompatActivity(), View.OnClickListener,
|
|||
val bitmap = createResizedBitmap(this, uriArg, resizeTo)
|
||||
if (bitmap != null) {
|
||||
try {
|
||||
val cacheDir = externalCacheDir?.apply{ mkdirs() }
|
||||
val cacheDir = externalCacheDir?.apply { mkdirs() }
|
||||
if (cacheDir == null) {
|
||||
showToast(false, "getExternalCacheDir returns null.")
|
||||
break
|
||||
|
|
|
@ -18,6 +18,7 @@ import jp.juggler.subwaytooter.action.saveWindowSize
|
|||
import jp.juggler.subwaytooter.actpost.*
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.databinding.ActPostBinding
|
||||
import jp.juggler.subwaytooter.dialog.*
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
import jp.juggler.subwaytooter.span.*
|
||||
|
@ -27,7 +28,11 @@ import jp.juggler.subwaytooter.view.MyEditText
|
|||
import jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||
import jp.juggler.util.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.delay
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class ActPost : AppCompatActivity(),
|
||||
|
@ -88,43 +93,11 @@ class ActPost : AppCompatActivity(),
|
|||
}
|
||||
}
|
||||
|
||||
lateinit var btnAccount: Button
|
||||
lateinit var btnVisibility: ImageButton
|
||||
private lateinit var btnAttachment: ImageButton
|
||||
private lateinit var btnPost: ImageButton
|
||||
lateinit var llAttachment: View
|
||||
val views by lazy { ActPostBinding.inflate(layoutInflater) }
|
||||
lateinit var ivMedia: List<MyNetworkImageView>
|
||||
lateinit var cbNSFW: CheckBox
|
||||
lateinit var cbContentWarning: CheckBox
|
||||
lateinit var etContentWarning: MyEditText
|
||||
lateinit var etContent: MyEditText
|
||||
|
||||
lateinit var cbQuote: CheckBox
|
||||
|
||||
lateinit var spPollType: Spinner
|
||||
lateinit var llEnquete: View
|
||||
lateinit var etChoices: List<MyEditText>
|
||||
|
||||
lateinit var cbMultipleChoice: CheckBox
|
||||
lateinit var cbHideTotals: CheckBox
|
||||
lateinit var llExpire: LinearLayout
|
||||
lateinit var etExpireDays: EditText
|
||||
lateinit var etExpireHours: EditText
|
||||
lateinit var etExpireMinutes: EditText
|
||||
|
||||
lateinit var tvCharCount: TextView
|
||||
lateinit var handler: Handler
|
||||
private lateinit var formRoot: ActPostRootLinearLayout
|
||||
|
||||
lateinit var llReply: View
|
||||
lateinit var tvReplyTo: TextView
|
||||
lateinit var ivReply: MyNetworkImageView
|
||||
lateinit var scrollView: ScrollView
|
||||
|
||||
lateinit var tvSchedule: TextView
|
||||
private lateinit var ibSchedule: ImageButton
|
||||
private lateinit var ibScheduleReset: ImageButton
|
||||
|
||||
lateinit var pref: SharedPreferences
|
||||
lateinit var appState: AppState
|
||||
lateinit var attachmentUploader: AttachmentUploader
|
||||
|
@ -133,6 +106,8 @@ class ActPost : AppCompatActivity(),
|
|||
|
||||
var density: Float = 0f
|
||||
|
||||
private lateinit var progressChannel : Channel<Unit>
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
|
||||
// SavedAccount.acctAscii => FeaturedTagCache
|
||||
|
@ -165,8 +140,8 @@ class ActPost : AppCompatActivity(),
|
|||
ar.data?.getStringExtra("replace_key")
|
||||
?.let { text ->
|
||||
when (states.mushroomInput) {
|
||||
0 -> applyMushroomText(etContent, text)
|
||||
1 -> applyMushroomText(etContentWarning, text)
|
||||
0 -> applyMushroomText(views.etContent, text)
|
||||
1 -> applyMushroomText(views.etContentWarning, text)
|
||||
else -> for (i in 0..3) {
|
||||
if (states.mushroomInput == i + 2) {
|
||||
applyMushroomText(etChoices[i], text)
|
||||
|
@ -191,6 +166,22 @@ class ActPost : AppCompatActivity(),
|
|||
density = resources.displayMetrics.density
|
||||
arMushroom.register(this, log)
|
||||
|
||||
progressChannel = Channel(capacity = Channel.CONFLATED )
|
||||
launchMain {
|
||||
try {
|
||||
while (true) {
|
||||
progressChannel.receive()
|
||||
showMedisAttachmentProgress()
|
||||
delay(1000L)
|
||||
}
|
||||
}catch(ex:Throwable){
|
||||
when(ex){
|
||||
is CancellationException, is ClosedReceiveChannelException -> Unit
|
||||
else -> log.trace(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initUI()
|
||||
|
||||
when (savedInstanceState) {
|
||||
|
@ -200,6 +191,11 @@ class ActPost : AppCompatActivity(),
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
try {
|
||||
progressChannel.close()
|
||||
}catch(ex:Throwable){
|
||||
log.e(ex)
|
||||
}
|
||||
completionHelper.onDestroy()
|
||||
attachmentUploader.onActivityDestroy()
|
||||
super.onDestroy()
|
||||
|
@ -269,7 +265,7 @@ class ActPost : AppCompatActivity(),
|
|||
return when {
|
||||
super.onKeyShortcut(keyCode, event) -> true
|
||||
event?.isCtrlPressed == true && keyCode == KeyEvent.KEYCODE_T -> {
|
||||
btnPost.performClick()
|
||||
views.btnPost.performClick()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
|
@ -283,7 +279,7 @@ class ActPost : AppCompatActivity(),
|
|||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
grantResults: IntArray,
|
||||
) {
|
||||
attachmentPicker.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
@ -293,6 +289,10 @@ class ActPost : AppCompatActivity(),
|
|||
addAttachment(uri, mimeType)
|
||||
}
|
||||
|
||||
override fun onPostAttachmentProgress() {
|
||||
launchIO{ progressChannel.send(Unit) }
|
||||
}
|
||||
|
||||
override fun onPostAttachmentComplete(pa: PostAttachment) {
|
||||
onPostAttachmentCompleteImpl(pa)
|
||||
}
|
||||
|
@ -302,7 +302,7 @@ class ActPost : AppCompatActivity(),
|
|||
}
|
||||
|
||||
fun initUI() {
|
||||
setContentView(R.layout.act_post)
|
||||
setContentView(views.root)
|
||||
App1.initEdgeToEdge(this)
|
||||
|
||||
if (PrefB.bpPostButtonBarTop(pref)) {
|
||||
|
@ -317,31 +317,19 @@ class ActPost : AppCompatActivity(),
|
|||
Styler.fixHorizontalMargin(findViewById(R.id.llFooterBar))
|
||||
}
|
||||
|
||||
formRoot = findViewById(R.id.viewRoot)
|
||||
scrollView = findViewById(R.id.scrollView)
|
||||
btnAccount = findViewById(R.id.btnAccount)
|
||||
btnVisibility = findViewById(R.id.btnVisibility)
|
||||
btnAttachment = findViewById(R.id.btnAttachment)
|
||||
btnPost = findViewById(R.id.btnPost)
|
||||
llAttachment = findViewById(R.id.llAttachment)
|
||||
cbNSFW = findViewById(R.id.cbNSFW)
|
||||
cbContentWarning = findViewById(R.id.cbContentWarning)
|
||||
etContentWarning = findViewById(R.id.etContentWarning)
|
||||
etContent = findViewById(R.id.etContent)
|
||||
|
||||
formRoot.callbackOnSizeChanged = { _, _, _, _ ->
|
||||
|
||||
views.root.callbackOnSizeChanged = { _, _, _, _ ->
|
||||
if (Build.VERSION.SDK_INT >= 24 && isMultiWindowPost) saveWindowSize()
|
||||
// ビューのw,hはシステムバーその他を含まないので使わない
|
||||
}
|
||||
|
||||
// https://github.com/tateisu/SubwayTooter/issues/123
|
||||
// 早い段階で指定する必要がある
|
||||
etContent.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
etContent.imeOptions = EditorInfo.IME_ACTION_NONE
|
||||
views.etContent.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
views.etContent.imeOptions = EditorInfo.IME_ACTION_NONE
|
||||
|
||||
cbQuote = findViewById(R.id.cbQuote)
|
||||
|
||||
spPollType = findViewById<Spinner>(R.id.spEnquete).apply {
|
||||
views.spPollType.apply {
|
||||
this.adapter = ArrayAdapter(
|
||||
this@ActPost,
|
||||
android.R.layout.simple_spinner_item,
|
||||
|
@ -364,7 +352,7 @@ class ActPost : AppCompatActivity(),
|
|||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
id: Long,
|
||||
) {
|
||||
showPoll()
|
||||
updateTextCount()
|
||||
|
@ -372,58 +360,44 @@ class ActPost : AppCompatActivity(),
|
|||
}
|
||||
}
|
||||
|
||||
llEnquete = findViewById(R.id.llEnquete)
|
||||
llExpire = findViewById(R.id.llExpire)
|
||||
cbHideTotals = findViewById(R.id.cbHideTotals)
|
||||
cbMultipleChoice = findViewById(R.id.cbMultipleChoice)
|
||||
etExpireDays = findViewById(R.id.etExpireDays)
|
||||
etExpireHours = findViewById(R.id.etExpireHours)
|
||||
etExpireMinutes = findViewById(R.id.etExpireMinutes)
|
||||
|
||||
ivMedia = listOf(
|
||||
findViewById(R.id.ivMedia1),
|
||||
findViewById(R.id.ivMedia2),
|
||||
findViewById(R.id.ivMedia3),
|
||||
findViewById(R.id.ivMedia4)
|
||||
views.ivMedia1,
|
||||
views.ivMedia2,
|
||||
views.ivMedia3,
|
||||
views.ivMedia4,
|
||||
)
|
||||
|
||||
etChoices = listOf(
|
||||
findViewById(R.id.etChoice1),
|
||||
findViewById(R.id.etChoice2),
|
||||
findViewById(R.id.etChoice3),
|
||||
findViewById(R.id.etChoice4)
|
||||
views.etChoice1,
|
||||
views.etChoice2,
|
||||
views.etChoice3,
|
||||
views.etChoice4,
|
||||
)
|
||||
|
||||
tvCharCount = findViewById(R.id.tvCharCount)
|
||||
llReply = findViewById(R.id.llReply)
|
||||
tvReplyTo = findViewById(R.id.tvReplyTo)
|
||||
ivReply = findViewById(R.id.ivReply)
|
||||
tvSchedule = findViewById(R.id.tvSchedule)
|
||||
ibSchedule = findViewById(R.id.ibSchedule)
|
||||
ibScheduleReset = findViewById(R.id.ibScheduleReset)
|
||||
|
||||
arrayOf(
|
||||
ibSchedule,
|
||||
ibScheduleReset,
|
||||
btnAccount,
|
||||
btnVisibility,
|
||||
btnAttachment,
|
||||
btnPost,
|
||||
findViewById(R.id.btnRemoveReply),
|
||||
findViewById(R.id.btnFeaturedTag),
|
||||
findViewById(R.id.btnPlugin),
|
||||
findViewById(R.id.btnEmojiPicker),
|
||||
findViewById(R.id.btnMore),
|
||||
views.ibSchedule,
|
||||
views.ibScheduleReset,
|
||||
views.btnAccount,
|
||||
views.btnVisibility,
|
||||
views.btnAttachment,
|
||||
views.btnPost,
|
||||
views.btnRemoveReply,
|
||||
views.btnFeaturedTag,
|
||||
views.btnPlugin,
|
||||
views.btnEmojiPicker,
|
||||
views.btnMore,
|
||||
).forEach { it.setOnClickListener(this) }
|
||||
|
||||
ivMedia.forEach { it.setOnClickListener(this) }
|
||||
|
||||
cbContentWarning.setOnCheckedChangeListener { _, _ -> showContentWarningEnabled() }
|
||||
views.cbContentWarning.setOnCheckedChangeListener { _, _ -> showContentWarningEnabled() }
|
||||
|
||||
completionHelper = CompletionHelper(this, pref, appState.handler)
|
||||
completionHelper.attachEditText(
|
||||
formRoot,
|
||||
etContent,
|
||||
views.root,
|
||||
views.etContent,
|
||||
false,
|
||||
object : CompletionHelper.Callback2 {
|
||||
override fun onTextUpdate() {
|
||||
|
@ -441,7 +415,7 @@ class ActPost : AppCompatActivity(),
|
|||
}
|
||||
}
|
||||
|
||||
etContentWarning.addTextChangedListener(textWatcher)
|
||||
views.etContentWarning.addTextChangedListener(textWatcher)
|
||||
|
||||
for (et in etChoices) {
|
||||
et.addTextChangedListener(textWatcher)
|
||||
|
@ -450,9 +424,9 @@ class ActPost : AppCompatActivity(),
|
|||
val scrollListener: ViewTreeObserver.OnScrollChangedListener =
|
||||
ViewTreeObserver.OnScrollChangedListener { completionHelper.onScrollChanged() }
|
||||
|
||||
scrollView.viewTreeObserver.addOnScrollChangedListener(scrollListener)
|
||||
views.scrollView.viewTreeObserver.addOnScrollChangedListener(scrollListener)
|
||||
|
||||
etContent.contentMineTypeArray = AttachmentUploader.acceptableMimeTypes.toTypedArray()
|
||||
etContent.contentCallback = { addAttachment(it) }
|
||||
views.etContent.contentMineTypeArray = AttachmentUploader.acceptableMimeTypes.toTypedArray()
|
||||
views.etContent.contentCallback = { addAttachment(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ fun ActPost.selectAccount(a: SavedAccount?) {
|
|||
completionHelper.setInstance(a)
|
||||
|
||||
if (a == null) {
|
||||
btnAccount.text = getString(R.string.not_selected)
|
||||
btnAccount.setTextColor(attrColor(android.R.attr.textColorPrimary))
|
||||
btnAccount.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
|
||||
views.btnAccount.text = getString(R.string.not_selected)
|
||||
views.btnAccount.setTextColor(attrColor(android.R.attr.textColorPrimary))
|
||||
views.btnAccount.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
|
||||
} else {
|
||||
|
||||
// 先読みしてキャッシュに保持しておく
|
||||
|
@ -29,16 +29,16 @@ fun ActPost.selectAccount(a: SavedAccount?) {
|
|||
}
|
||||
|
||||
val ac = AcctColor.load(a)
|
||||
btnAccount.text = ac.nickname
|
||||
views.btnAccount.text = ac.nickname
|
||||
|
||||
if (AcctColor.hasColorBackground(ac)) {
|
||||
btnAccount.background =
|
||||
views.btnAccount.background =
|
||||
getAdaptiveRippleDrawableRound(this, ac.color_bg, ac.color_fg)
|
||||
} else {
|
||||
btnAccount.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
|
||||
views.btnAccount.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
|
||||
}
|
||||
|
||||
btnAccount.textColor = ac.color_fg.notZero()
|
||||
views.btnAccount.textColor = ac.color_fg.notZero()
|
||||
?: attrColor(android.R.attr.textColorPrimary)
|
||||
}
|
||||
updateTextCount()
|
||||
|
|
|
@ -44,10 +44,19 @@ fun ActPost.decodeAttachments(sv: String) {
|
|||
|
||||
fun ActPost.showMediaAttachment() {
|
||||
if (isFinishing) return
|
||||
llAttachment.vg(attachmentList.isNotEmpty())
|
||||
views.llAttachment.vg(attachmentList.isNotEmpty())
|
||||
ivMedia.forEachIndexed { i, v -> showMediaAttachmentOne(v, i) }
|
||||
}
|
||||
|
||||
fun ActPost.showMedisAttachmentProgress() {
|
||||
val mergedProgress = attachmentList
|
||||
.mapNotNull { it.progress.notEmpty() }
|
||||
.joinToString("\n")
|
||||
views.tvAttachmentProgress
|
||||
.vg(mergedProgress.isNotEmpty())
|
||||
?.text = mergedProgress
|
||||
}
|
||||
|
||||
fun ActPost.showMediaAttachmentOne(iv: MyNetworkImageView, idx: Int) {
|
||||
if (idx >= attachmentList.size) {
|
||||
iv.visibility = View.GONE
|
||||
|
@ -118,7 +127,7 @@ fun ActPost.addAttachment(
|
|||
uri,
|
||||
mimeType,
|
||||
isReply = isReply,
|
||||
// onUploadEnd = onUploadEnd
|
||||
// onUploadEnd = onUploadEnd
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -153,9 +162,9 @@ fun ActPost.onPostAttachmentCompleteImpl(pa: PostAttachment) {
|
|||
|
||||
// 投稿欄の末尾に追記する
|
||||
if (PrefB.bpAppendAttachmentUrlToContent(pref)) {
|
||||
val selStart = etContent.selectionStart
|
||||
val selEnd = etContent.selectionEnd
|
||||
val e = etContent.editableText
|
||||
val selStart = views.etContent.selectionStart
|
||||
val selEnd = views.etContent.selectionEnd
|
||||
val e = views.etContent.editableText
|
||||
val len = e.length
|
||||
val lastChar = if (len <= 0) ' ' else e[len - 1]
|
||||
if (!CharacterGroup.isWhitespace(lastChar.code)) {
|
||||
|
@ -163,7 +172,7 @@ fun ActPost.onPostAttachmentCompleteImpl(pa: PostAttachment) {
|
|||
} else {
|
||||
e.append(a.text_url)
|
||||
}
|
||||
etContent.setSelection(selStart, selEnd)
|
||||
views.etContent.setSelection(selStart, selEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -215,6 +224,9 @@ fun ActPost.deleteAttachment(pa: PostAttachment) {
|
|||
.setTitle(R.string.confirm_delete_attachment)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
try {
|
||||
pa.isCancelled = true
|
||||
pa.status= PostAttachment.Status.Error
|
||||
pa.job.cancel()
|
||||
attachmentList.remove(pa)
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
|
|
|
@ -57,12 +57,12 @@ fun ActPost.updateTextCount() {
|
|||
var length = 0
|
||||
|
||||
length += TootAccount.countText(
|
||||
EmojiDecoder.decodeShortCode(etContent.text.toString())
|
||||
EmojiDecoder.decodeShortCode(views.etContent.text.toString())
|
||||
)
|
||||
|
||||
if (cbContentWarning.isChecked) {
|
||||
if (views.cbContentWarning.isChecked) {
|
||||
length += TootAccount.countText(
|
||||
EmojiDecoder.decodeShortCode(etContentWarning.text.toString())
|
||||
EmojiDecoder.decodeShortCode(views.etContentWarning.text.toString())
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ fun ActPost.updateTextCount() {
|
|||
}
|
||||
}
|
||||
|
||||
when (spPollType.selectedItemPosition) {
|
||||
when (views.spPollType.selectedItemPosition) {
|
||||
1 -> checkEnqueteLength()
|
||||
|
||||
2 -> {
|
||||
|
@ -87,8 +87,8 @@ fun ActPost.updateTextCount() {
|
|||
|
||||
val remain = max - length
|
||||
|
||||
tvCharCount.text = remain.toString()
|
||||
tvCharCount.setTextColor(
|
||||
views.tvCharCount.text = remain.toString()
|
||||
views.tvCharCount.setTextColor(
|
||||
attrColor(
|
||||
when {
|
||||
remain < 0 -> R.attr.colorRegexFilterError
|
||||
|
|
|
@ -71,11 +71,11 @@ private suspend fun checkExist(url: String?): Boolean {
|
|||
}
|
||||
|
||||
fun ActPost.saveDraft() {
|
||||
val content = etContent.text.toString()
|
||||
val content = views.etContent.text.toString()
|
||||
val contentWarning =
|
||||
if (cbContentWarning.isChecked) etContentWarning.text.toString() else ""
|
||||
if (views.cbContentWarning.isChecked) views.etContentWarning.text.toString() else ""
|
||||
|
||||
val isEnquete = spPollType.selectedItemPosition > 0
|
||||
val isEnquete = views.spPollType.selectedItemPosition > 0
|
||||
|
||||
val strChoice = arrayOf(
|
||||
if (isEnquete) etChoices[0].text.toString() else "",
|
||||
|
@ -104,20 +104,20 @@ fun ActPost.saveDraft() {
|
|||
val json = JsonObject()
|
||||
json[DRAFT_CONTENT] = content
|
||||
json[DRAFT_CONTENT_WARNING] = contentWarning
|
||||
json[DRAFT_CONTENT_WARNING_CHECK] = cbContentWarning.isChecked
|
||||
json[DRAFT_NSFW_CHECK] = cbNSFW.isChecked
|
||||
json[DRAFT_CONTENT_WARNING_CHECK] = views.cbContentWarning.isChecked
|
||||
json[DRAFT_NSFW_CHECK] = views.cbNSFW.isChecked
|
||||
json[DRAFT_ACCOUNT_DB_ID] = account?.db_id ?: -1L
|
||||
json[DRAFT_ATTACHMENT_LIST] = tmpAttachmentList
|
||||
json[DRAFT_REPLY_TEXT] = states.inReplyToText
|
||||
json[DRAFT_REPLY_IMAGE] = states.inReplyToImage
|
||||
json[DRAFT_REPLY_URL] = states.inReplyToUrl
|
||||
json[DRAFT_QUOTE] = cbQuote.isChecked
|
||||
json[DRAFT_POLL_TYPE] = spPollType.selectedItemPosition.toPollTypeString()
|
||||
json[DRAFT_POLL_MULTIPLE] = cbMultipleChoice.isChecked
|
||||
json[DRAFT_POLL_HIDE_TOTALS] = cbHideTotals.isChecked
|
||||
json[DRAFT_POLL_EXPIRE_DAY] = etExpireDays.text.toString()
|
||||
json[DRAFT_POLL_EXPIRE_HOUR] = etExpireHours.text.toString()
|
||||
json[DRAFT_POLL_EXPIRE_MINUTE] = etExpireMinutes.text.toString()
|
||||
json[DRAFT_QUOTE] = views.cbQuote.isChecked
|
||||
json[DRAFT_POLL_TYPE] = views.spPollType.selectedItemPosition.toPollTypeString()
|
||||
json[DRAFT_POLL_MULTIPLE] = views.cbMultipleChoice.isChecked
|
||||
json[DRAFT_POLL_HIDE_TOTALS] = views.cbHideTotals.isChecked
|
||||
json[DRAFT_POLL_EXPIRE_DAY] = views.etExpireDays.text.toString()
|
||||
json[DRAFT_POLL_EXPIRE_HOUR] = views.etExpireHours.text.toString()
|
||||
json[DRAFT_POLL_EXPIRE_MINUTE] = views.etExpireMinutes.text.toString()
|
||||
json[DRAFT_ENQUETE_ITEMS] = strChoice.toJsonArray()
|
||||
|
||||
states.visibility?.id?.toString()?.let { json.put(DRAFT_VISIBILITY, it) }
|
||||
|
@ -242,30 +242,30 @@ fun ActPost.restoreDraft(draft: JsonObject) {
|
|||
val evEmoji = DecodeOptions(this@restoreDraft, decodeEmoji = true)
|
||||
.decodeEmoji(content)
|
||||
|
||||
etContent.setText(evEmoji)
|
||||
etContent.setSelection(evEmoji.length)
|
||||
etContentWarning.setText(contentWarning)
|
||||
etContentWarning.setSelection(contentWarning.length)
|
||||
cbContentWarning.isChecked = contentWarningChecked
|
||||
cbNSFW.isChecked = nsfwChecked
|
||||
views.etContent.setText(evEmoji)
|
||||
views.etContent.setSelection(evEmoji.length)
|
||||
views.etContentWarning.setText(contentWarning)
|
||||
views.etContentWarning.setSelection(contentWarning.length)
|
||||
views.cbContentWarning.isChecked = contentWarningChecked
|
||||
views.cbNSFW.isChecked = nsfwChecked
|
||||
if (draftVisibility != null) states.visibility = draftVisibility
|
||||
|
||||
cbQuote.isChecked = draft.optBoolean(DRAFT_QUOTE)
|
||||
views.cbQuote.isChecked = draft.optBoolean(DRAFT_QUOTE)
|
||||
|
||||
val sv = draft.string(DRAFT_POLL_TYPE)
|
||||
if (sv != null) {
|
||||
spPollType.setSelection(sv.toPollTypeIndex())
|
||||
views.spPollType.setSelection(sv.toPollTypeIndex())
|
||||
} else {
|
||||
// old draft
|
||||
val bv = draft.optBoolean(DRAFT_IS_ENQUETE, false)
|
||||
spPollType.setSelection(if (bv) 2 else 0)
|
||||
views.spPollType.setSelection(if (bv) 2 else 0)
|
||||
}
|
||||
|
||||
cbMultipleChoice.isChecked = draft.optBoolean(DRAFT_POLL_MULTIPLE)
|
||||
cbHideTotals.isChecked = draft.optBoolean(DRAFT_POLL_HIDE_TOTALS)
|
||||
etExpireDays.setText(draft.optString(DRAFT_POLL_EXPIRE_DAY, "1"))
|
||||
etExpireHours.setText(draft.optString(DRAFT_POLL_EXPIRE_HOUR, ""))
|
||||
etExpireMinutes.setText(draft.optString(DRAFT_POLL_EXPIRE_MINUTE, ""))
|
||||
views.cbMultipleChoice.isChecked = draft.optBoolean(DRAFT_POLL_MULTIPLE)
|
||||
views.cbHideTotals.isChecked = draft.optBoolean(DRAFT_POLL_HIDE_TOTALS)
|
||||
views.etExpireDays.setText(draft.optString(DRAFT_POLL_EXPIRE_DAY, "1"))
|
||||
views.etExpireHours.setText(draft.optString(DRAFT_POLL_EXPIRE_HOUR, ""))
|
||||
views.etExpireMinutes.setText(draft.optString(DRAFT_POLL_EXPIRE_MINUTE, ""))
|
||||
|
||||
val array = draft.jsonArray(DRAFT_ENQUETE_ITEMS)
|
||||
if (array != null) {
|
||||
|
@ -350,7 +350,7 @@ fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String)
|
|||
}
|
||||
}
|
||||
|
||||
cbNSFW.isChecked = baseStatus.sensitive == true
|
||||
views.cbNSFW.isChecked = baseStatus.sensitive == true
|
||||
|
||||
// 再編集の場合はdefault_textは反映されない
|
||||
|
||||
|
@ -366,13 +366,13 @@ fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String)
|
|||
} else {
|
||||
decodeOptions.decodeHTML(baseStatus.content)
|
||||
}
|
||||
etContent.setText(text)
|
||||
etContent.setSelection(text.length)
|
||||
views.etContent.setText(text)
|
||||
views.etContent.setSelection(text.length)
|
||||
|
||||
text = decodeOptions.decodeEmoji(baseStatus.spoiler_text)
|
||||
etContentWarning.setText(text)
|
||||
etContentWarning.setSelection(text.length)
|
||||
cbContentWarning.isChecked = text.isNotEmpty()
|
||||
views.etContentWarning.setText(text)
|
||||
views.etContentWarning.setSelection(text.length)
|
||||
views.cbContentWarning.isChecked = text.isNotEmpty()
|
||||
|
||||
val srcEnquete = baseStatus.enquete
|
||||
val srcItems = srcEnquete?.items
|
||||
|
@ -387,7 +387,7 @@ fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String)
|
|||
}
|
||||
|
||||
else -> {
|
||||
spPollType.setSelection(
|
||||
views.spPollType.setSelection(
|
||||
if (srcEnquete.pollType == TootPollsType.FriendsNico) {
|
||||
2
|
||||
} else {
|
||||
|
@ -395,8 +395,8 @@ fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String)
|
|||
}
|
||||
)
|
||||
text = decodeOptions.decodeHTML(srcEnquete.question)
|
||||
etContent.text = text
|
||||
etContent.setSelection(text.length)
|
||||
views.etContent.text = text
|
||||
views.etContent.setSelection(text.length)
|
||||
|
||||
var srcIndex = 0
|
||||
for (et in etChoices) {
|
||||
|
|
|
@ -2,7 +2,6 @@ package jp.juggler.subwaytooter.actpost
|
|||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.*
|
||||
|
@ -29,19 +28,19 @@ fun ActPost.appendContentText(
|
|||
).decodeEmoji(src)
|
||||
if (svEmoji.isEmpty()) return
|
||||
|
||||
val editable = etContent.text
|
||||
val editable = views.etContent.text
|
||||
if (editable == null) {
|
||||
val sb = StringBuilder()
|
||||
if (selectBefore) {
|
||||
val start = 0
|
||||
sb.append(' ')
|
||||
sb.append(svEmoji)
|
||||
etContent.setText(sb)
|
||||
etContent.setSelection(start)
|
||||
views.etContent.setText(sb)
|
||||
views.etContent.setSelection(start)
|
||||
} else {
|
||||
sb.append(svEmoji)
|
||||
etContent.setText(sb)
|
||||
etContent.setSelection(sb.length)
|
||||
views.etContent.setText(sb)
|
||||
views.etContent.setSelection(sb.length)
|
||||
}
|
||||
} else {
|
||||
if (editable.isNotEmpty() &&
|
||||
|
@ -54,12 +53,12 @@ fun ActPost.appendContentText(
|
|||
val start = editable.length
|
||||
editable.append(' ')
|
||||
editable.append(svEmoji)
|
||||
etContent.text = editable
|
||||
etContent.setSelection(start)
|
||||
views.etContent.text = editable
|
||||
views.etContent.setSelection(start)
|
||||
} else {
|
||||
editable.append(svEmoji)
|
||||
etContent.text = editable
|
||||
etContent.setSelection(editable.length)
|
||||
views.etContent.text = editable
|
||||
views.etContent.setSelection(editable.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +79,9 @@ fun ActPost.appendContentText(src: Intent) {
|
|||
|
||||
// returns true if has content
|
||||
fun ActPost.hasContent(): Boolean {
|
||||
val content = etContent.text.toString()
|
||||
val content = views.etContent.text.toString()
|
||||
val contentWarning =
|
||||
if (cbContentWarning.isChecked) etContentWarning.text.toString() else ""
|
||||
if (views.cbContentWarning.isChecked) views.etContentWarning.text.toString() else ""
|
||||
|
||||
return when {
|
||||
content.isNotBlank() -> true
|
||||
|
@ -103,9 +102,9 @@ fun ActPost.resetText() {
|
|||
attachmentPicker.reset()
|
||||
scheduledStatus = null
|
||||
attachmentList.clear()
|
||||
cbQuote.isChecked = false
|
||||
etContent.setText("")
|
||||
spPollType.setSelection(0, false)
|
||||
views.cbQuote.isChecked = false
|
||||
views.etContent.setText("")
|
||||
views.spPollType.setSelection(0, false)
|
||||
etChoices.forEach { it.setText("") }
|
||||
accountList = SavedAccount.loadAccountList(this)
|
||||
SavedAccount.sort(accountList)
|
||||
|
@ -161,7 +160,7 @@ fun ActPost.updateText(
|
|||
resetText()
|
||||
|
||||
// Android 9 から、明示的にフォーカスを当てる必要がある
|
||||
etContent.requestFocus()
|
||||
views.etContent.requestFocus()
|
||||
|
||||
this.attachmentList.clear()
|
||||
saveAttachmentList()
|
||||
|
@ -188,7 +187,7 @@ fun ActPost.updateText(
|
|||
}
|
||||
|
||||
appendContentText(account?.default_text, selectBefore = true)
|
||||
cbNSFW.isChecked = account?.default_sensitive ?: false
|
||||
views.cbNSFW.isChecked = account?.default_sensitive ?: false
|
||||
|
||||
if (account != null) {
|
||||
// 再編集
|
||||
|
@ -261,13 +260,13 @@ fun ActPost.performMore() {
|
|||
}
|
||||
|
||||
dialog.addAction(getString(R.string.clear_text)) {
|
||||
etContent.setText("")
|
||||
etContentWarning.setText("")
|
||||
views.etContent.setText("")
|
||||
views.etContentWarning.setText("")
|
||||
}
|
||||
|
||||
dialog.addAction(getString(R.string.clear_text_and_media)) {
|
||||
etContent.setText("")
|
||||
etContentWarning.setText("")
|
||||
views.etContent.setText("")
|
||||
views.etContentWarning.setText("")
|
||||
attachmentList.clear()
|
||||
showMediaAttachment()
|
||||
}
|
||||
|
@ -296,13 +295,13 @@ fun ActPost.performPost() {
|
|||
var pollExpireSeconds = 0
|
||||
var pollHideTotals = false
|
||||
var pollMultipleChoice = false
|
||||
when (spPollType.selectedItemPosition) {
|
||||
when (views.spPollType.selectedItemPosition) {
|
||||
1 -> {
|
||||
pollType = TootPollsType.Mastodon
|
||||
pollItems = pollChoiceList()
|
||||
pollExpireSeconds = pollExpireSeconds()
|
||||
pollHideTotals = cbHideTotals.isChecked
|
||||
pollMultipleChoice = cbMultipleChoice.isChecked
|
||||
pollHideTotals = views.cbHideTotals.isChecked
|
||||
pollMultipleChoice = views.cbMultipleChoice.isChecked
|
||||
}
|
||||
2 -> {
|
||||
pollType = TootPollsType.FriendsNico
|
||||
|
@ -313,13 +312,13 @@ fun ActPost.performPost() {
|
|||
PostImpl(
|
||||
activity = this,
|
||||
account = account,
|
||||
content = etContent.text.toString().trim { it <= ' ' },
|
||||
content = views.etContent.text.toString().trim { it <= ' ' },
|
||||
spoilerText = when {
|
||||
!cbContentWarning.isChecked -> null
|
||||
else -> etContentWarning.text.toString().trim { it <= ' ' }
|
||||
!views.cbContentWarning.isChecked -> null
|
||||
else -> views.etContentWarning.text.toString().trim { it <= ' ' }
|
||||
},
|
||||
visibilityArg = states.visibility ?: TootVisibility.Public,
|
||||
bNSFW = cbNSFW.isChecked,
|
||||
bNSFW = views.cbNSFW.isChecked,
|
||||
inReplyToId = states.inReplyToId,
|
||||
attachmentListArg = this.attachmentList,
|
||||
enqueteItemsArg = pollItems,
|
||||
|
@ -331,7 +330,7 @@ fun ActPost.performPost() {
|
|||
scheduledId = scheduledStatus?.id,
|
||||
redraftStatusId = states.redraftStatusId,
|
||||
emojiMapCustom = App1.custom_emoji_lister.getMap(account),
|
||||
useQuoteToot = cbQuote.isChecked,
|
||||
useQuoteToot = views.cbQuote.isChecked,
|
||||
callback = object : PostCompleteCallback {
|
||||
override fun onPostComplete(targetAccount: SavedAccount, status: TootStatus) {
|
||||
val data = Intent()
|
||||
|
@ -374,5 +373,5 @@ fun ActPost.performPost() {
|
|||
}
|
||||
|
||||
fun ActPost.showContentWarningEnabled() {
|
||||
etContentWarning.visibility = if (cbContentWarning.isChecked) View.VISIBLE else View.GONE
|
||||
views.etContentWarning.vg(views.cbContentWarning.isChecked)
|
||||
}
|
||||
|
|
|
@ -63,14 +63,14 @@ fun ActPost.openMushroom() {
|
|||
try {
|
||||
var text: String? = null
|
||||
when {
|
||||
etContentWarning.hasFocus() -> {
|
||||
views.etContentWarning.hasFocus() -> {
|
||||
states.mushroomInput = 1
|
||||
text = prepareMushroomText(etContentWarning)
|
||||
text = prepareMushroomText(views.etContentWarning)
|
||||
}
|
||||
|
||||
etContent.hasFocus() -> {
|
||||
views.etContent.hasFocus() -> {
|
||||
states.mushroomInput = 0
|
||||
text = prepareMushroomText(etContent)
|
||||
text = prepareMushroomText(views.etContent)
|
||||
}
|
||||
|
||||
else -> for (i in 0..3) {
|
||||
|
@ -82,7 +82,7 @@ fun ActPost.openMushroom() {
|
|||
}
|
||||
if (text == null) {
|
||||
states.mushroomInput = 0
|
||||
text = prepareMushroomText(etContent)
|
||||
text = prepareMushroomText(views.etContent)
|
||||
}
|
||||
|
||||
val intent = Intent("com.adamrocker.android.simeji.ACTION_INTERCEPT")
|
||||
|
|
|
@ -7,16 +7,16 @@ import jp.juggler.util.vg
|
|||
private fun Double?.finiteOrZero(): Double = if (this?.isFinite() == true) this else 0.0
|
||||
|
||||
fun ActPost.showPoll() {
|
||||
val i = spPollType.selectedItemPosition
|
||||
llEnquete.vg(i != 0)
|
||||
llExpire.vg(i == 1)
|
||||
cbHideTotals.vg(i == 1)
|
||||
cbMultipleChoice.vg(i == 1)
|
||||
val i = views.spPollType.selectedItemPosition
|
||||
views.llEnquete.vg(i != 0)
|
||||
views.llExpire.vg(i == 1)
|
||||
views.cbHideTotals.vg(i == 1)
|
||||
views.cbMultipleChoice.vg(i == 1)
|
||||
}
|
||||
|
||||
// 投票が有効で何か入力済みなら真
|
||||
fun ActPost.hasPoll(): Boolean {
|
||||
if (spPollType.selectedItemPosition <= 0) return false
|
||||
if (views.spPollType.selectedItemPosition <= 0) return false
|
||||
return etChoices.any { it.text.toString().isNotBlank() }
|
||||
}
|
||||
|
||||
|
@ -27,8 +27,8 @@ fun ActPost.pollChoiceList() = ArrayList<String>().apply {
|
|||
}
|
||||
|
||||
fun ActPost.pollExpireSeconds(): Int {
|
||||
val d = etExpireDays.text.toString().trim().toDoubleOrNull().finiteOrZero()
|
||||
val h = etExpireHours.text.toString().trim().toDoubleOrNull().finiteOrZero()
|
||||
val m = etExpireMinutes.text.toString().trim().toDoubleOrNull().finiteOrZero()
|
||||
val d = views.etExpireDays.text.toString().trim().toDoubleOrNull().finiteOrZero()
|
||||
val h = views.etExpireHours.text.toString().trim().toDoubleOrNull().finiteOrZero()
|
||||
val m = views.etExpireMinutes.text.toString().trim().toDoubleOrNull().finiteOrZero()
|
||||
return (d * 86400.0 + h * 3600.0 + m * 60.0).toInt()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package jp.juggler.subwaytooter.actpost
|
||||
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import jp.juggler.subwaytooter.ActPost
|
||||
import jp.juggler.subwaytooter.R
|
||||
|
@ -14,10 +13,7 @@ import jp.juggler.subwaytooter.api.runApiTask
|
|||
import jp.juggler.subwaytooter.api.syncStatus
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.DecodeOptions
|
||||
import jp.juggler.util.LogCategory
|
||||
import jp.juggler.util.decodeJsonObject
|
||||
import jp.juggler.util.launchMain
|
||||
import jp.juggler.util.showToast
|
||||
import jp.juggler.util.*
|
||||
|
||||
private val log = LogCategory("ActPostReply")
|
||||
|
||||
|
@ -29,22 +25,20 @@ fun ActPost.resetReply() {
|
|||
}
|
||||
|
||||
fun ActPost.showQuotedRenote() {
|
||||
cbQuote.visibility = if (states.inReplyToId != null) View.VISIBLE else View.GONE
|
||||
views.cbQuote.vg(states.inReplyToId != null)
|
||||
}
|
||||
|
||||
fun ActPost.showReplyTo() {
|
||||
if (states.inReplyToId == null) {
|
||||
llReply.visibility = View.GONE
|
||||
} else {
|
||||
llReply.visibility = View.VISIBLE
|
||||
tvReplyTo.text = DecodeOptions(
|
||||
views.llReply.vg(states.inReplyToId != null)?.let {
|
||||
views.tvReplyTo.text = DecodeOptions(
|
||||
this,
|
||||
linkHelper = account,
|
||||
short = true,
|
||||
decodeEmoji = true,
|
||||
mentionDefaultHostDomain = account ?: unknownHostAndDomain
|
||||
).decodeHTML(states.inReplyToText)
|
||||
ivReply.setImageUrl(Styler.calcIconRound(ivReply.layoutParams), states.inReplyToImage)
|
||||
views.ivReply.setImageUrl(Styler.calcIconRound(views.ivReply.layoutParams),
|
||||
states.inReplyToImage)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,15 +59,15 @@ fun ActPost.initializeFromReplyStatus(account: SavedAccount, jsonText: String) {
|
|||
|
||||
val isQuote = intent.getBooleanExtra(ActPost.KEY_QUOTE, false)
|
||||
if (isQuote) {
|
||||
cbQuote.isChecked = true
|
||||
views.cbQuote.isChecked = true
|
||||
|
||||
// 引用リノートはCWやメンションを引き継がない
|
||||
} else {
|
||||
|
||||
// CW をリプライ元に合わせる
|
||||
if (replyStatus.spoiler_text.isNotEmpty()) {
|
||||
cbContentWarning.isChecked = true
|
||||
etContentWarning.setText(replyStatus.spoiler_text)
|
||||
views.cbContentWarning.isChecked = true
|
||||
views.etContentWarning.setText(replyStatus.spoiler_text)
|
||||
}
|
||||
|
||||
// 新しいメンションリスト
|
||||
|
|
|
@ -18,7 +18,7 @@ import jp.juggler.util.notEmpty
|
|||
private val log = LogCategory("ActPostSchedule")
|
||||
|
||||
fun ActPost.showSchedule() {
|
||||
tvSchedule.text = when (states.timeSchedule) {
|
||||
views.tvSchedule.text = when (states.timeSchedule) {
|
||||
0L -> getString(R.string.unspecified)
|
||||
else -> TootStatus.formatTime(this, states.timeSchedule, true)
|
||||
}
|
||||
|
@ -49,13 +49,13 @@ fun ActPost.initializeFromScheduledStatus(account: SavedAccount, jsonText: Strin
|
|||
|
||||
states.timeSchedule = item.timeScheduledAt
|
||||
states.visibility = item.visibility
|
||||
cbNSFW.isChecked = item.sensitive
|
||||
views.cbNSFW.isChecked = item.sensitive
|
||||
|
||||
etContent.setText(item.text)
|
||||
views.etContent.setText(item.text)
|
||||
|
||||
val cw = item.spoilerText
|
||||
etContentWarning.setText(cw ?: "")
|
||||
cbContentWarning.isChecked = cw?.isNotEmpty() == true
|
||||
views.etContentWarning.setText(cw ?: "")
|
||||
views.cbContentWarning.isChecked = cw?.isNotEmpty() == true
|
||||
|
||||
// 2019/1/7 どうも添付データを古い投稿から引き継げないようだ…。
|
||||
// 2019/1/22 https://github.com/tootsuite/mastodon/pull/9894 で直った。
|
||||
|
|
|
@ -9,8 +9,9 @@ import jp.juggler.subwaytooter.api.entity.TootInstance
|
|||
import jp.juggler.subwaytooter.api.entity.TootVisibility
|
||||
|
||||
fun ActPost.showVisibility() {
|
||||
val iconId = Styler.getVisibilityIconId(account?.isMisskey == true, states.visibility ?: TootVisibility.Public)
|
||||
btnVisibility.setImageResource(iconId)
|
||||
val iconId = Styler.getVisibilityIconId(account?.isMisskey == true,
|
||||
states.visibility ?: TootVisibility.Public)
|
||||
views.btnVisibility.setImageResource(iconId)
|
||||
}
|
||||
|
||||
fun ActPost.openVisibilityPicker() {
|
||||
|
|
|
@ -59,8 +59,9 @@ import jp.juggler.util.LogCategory
|
|||
// 2021/5/11 59=>60 SavedAccountテーブルに項目追加
|
||||
// 2021/5/23 60=>61 SavedAccountテーブルに項目追加
|
||||
// 2021/11/21 61=>62 SavedAccountテーブルに項目追加
|
||||
// 2022/1/5 62=>63 SavedAccountテーブルに項目追加
|
||||
|
||||
const val DB_VERSION = 62
|
||||
const val DB_VERSION = 63
|
||||
const val DB_NAME = "app_db"
|
||||
|
||||
val TABLE_LIST = arrayOf(
|
||||
|
|
|
@ -85,6 +85,26 @@ class SavedAccount(
|
|||
|
||||
var push_policy: String? = null
|
||||
|
||||
private val extraJson = JsonObject()
|
||||
|
||||
var movieTranscodeMode: Int by JsonProperty(
|
||||
extraJson,
|
||||
"movieTranscodeMode",
|
||||
0)
|
||||
|
||||
var movieTranscodeBitrate: String by JsonProperty(
|
||||
extraJson,
|
||||
"movieTranscodeBitrate",
|
||||
"2000000")
|
||||
var movieTranscodeFramerate: String by JsonProperty(
|
||||
extraJson,
|
||||
"movieTranscodeFramerate",
|
||||
"30")
|
||||
var movieTranscodeSquarePixels: String by JsonProperty(
|
||||
extraJson,
|
||||
"movieTranscodeSquarePixels",
|
||||
"2304000")
|
||||
|
||||
init {
|
||||
val tmpAcct = Acct.parse(acctArg)
|
||||
this.username = tmpAcct.username
|
||||
|
@ -173,6 +193,14 @@ class SavedAccount(
|
|||
image_max_megabytes = cursor.getStringOrNull(COL_IMAGE_MAX_MEGABYTES)
|
||||
movie_max_megabytes = cursor.getStringOrNull(COL_MOVIE_MAX_MEGABYTES)
|
||||
push_policy = cursor.getStringOrNull(COL_PUSH_POLICY)
|
||||
try {
|
||||
cursor.getStringOrNull(COL_EXTRA_JSON)
|
||||
?.decodeJsonObject()
|
||||
?.entries
|
||||
?.forEach { extraJson[it.key] = it.value }
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
}
|
||||
|
||||
val isNA: Boolean
|
||||
|
@ -242,6 +270,7 @@ class SavedAccount(
|
|||
put(COL_IMAGE_MAX_MEGABYTES, image_max_megabytes)
|
||||
put(COL_MOVIE_MAX_MEGABYTES, movie_max_megabytes)
|
||||
put(COL_PUSH_POLICY, push_policy)
|
||||
put(COL_EXTRA_JSON, extraJson.toString())
|
||||
|
||||
// 以下のデータはUIからは更新しない
|
||||
// notification_tag
|
||||
|
@ -310,6 +339,11 @@ class SavedAccount(
|
|||
this.image_max_megabytes = b.image_max_megabytes
|
||||
this.movie_max_megabytes = b.movie_max_megabytes
|
||||
this.push_policy = b.push_policy
|
||||
|
||||
this.movieTranscodeMode = b.movieTranscodeMode
|
||||
this.movieTranscodeBitrate = b.movieTranscodeBitrate
|
||||
this.movieTranscodeFramerate = b.movieTranscodeFramerate
|
||||
this.movieTranscodeSquarePixels = b.movieTranscodeSquarePixels
|
||||
}
|
||||
|
||||
fun getFullAcct(who: TootAccount?) = getFullAcct(who?.acct)
|
||||
|
@ -458,6 +492,8 @@ class SavedAccount(
|
|||
|
||||
private val COL_PUSH_POLICY = ColumnMeta(columnList, 60, "push_policy", "text default null")
|
||||
|
||||
private val COL_EXTRA_JSON = ColumnMeta(columnList, 63, "extra_json", "text default null")
|
||||
|
||||
/////////////////////////////////
|
||||
// login information
|
||||
const val INVALID_DB_ID = -1L
|
||||
|
@ -930,4 +966,20 @@ class SavedAccount(
|
|||
this.image_max_megabytes?.toIntOrNull()
|
||||
?: if (ti.instanceType == InstanceType.Pixelfed) 15 else 8
|
||||
)
|
||||
|
||||
fun getMovieResizeConfig(): MovieResizeConfig = MovieResizeConfig(
|
||||
mode = when (movieTranscodeMode) {
|
||||
MovieResizeConfig.MODE_NO,
|
||||
MovieResizeConfig.MODE_AUTO,
|
||||
MovieResizeConfig.NODE_ALWAYS,
|
||||
-> movieTranscodeMode
|
||||
else -> MovieResizeConfig.MODE_AUTO
|
||||
},
|
||||
limitBitrate = movieTranscodeBitrate.toLongOrNull()
|
||||
?.takeIf { it >= 100_000L } ?: 2_000_000L,
|
||||
limitFrameRate = movieTranscodeFramerate.toIntOrNull()
|
||||
?.takeIf { it >= 1 } ?: 30,
|
||||
limitPixelMatrix = movieTranscodeSquarePixels.toIntOrNull()
|
||||
?.takeIf { it > 0 } ?: 2304000,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import java.io.*
|
|||
import java.util.*
|
||||
import java.util.concurrent.CancellationException
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.math.min
|
||||
|
||||
class AttachmentRequest(
|
||||
val account: SavedAccount,
|
||||
|
@ -37,7 +38,6 @@ class AttachmentRequest(
|
|||
val uri: Uri,
|
||||
val mimeType: String,
|
||||
val isReply: Boolean,
|
||||
val onUploadEnd: () -> Unit = {},
|
||||
)
|
||||
|
||||
class AttachmentUploader(
|
||||
|
@ -92,7 +92,7 @@ class AttachmentUploader(
|
|||
add("video/m4v")
|
||||
}
|
||||
|
||||
private val imageHeaderList = arrayOf(
|
||||
private val imageHeaderList = listOf(
|
||||
Pair(
|
||||
"image/jpeg",
|
||||
intArrayOf(0xff, 0xd8, 0xff).toByteArray()
|
||||
|
@ -103,21 +103,29 @@ class AttachmentUploader(
|
|||
),
|
||||
Pair(
|
||||
"image/gif",
|
||||
charArrayOf('G', 'I', 'F').toLowerByteArray()
|
||||
"GIF".toByteArray(Charsets.UTF_8)
|
||||
),
|
||||
Pair(
|
||||
"audio/wav",
|
||||
charArrayOf('R', 'I', 'F', 'F').toLowerByteArray()
|
||||
"RIFF".toByteArray(Charsets.UTF_8),
|
||||
),
|
||||
Pair(
|
||||
"audio/ogg",
|
||||
charArrayOf('O', 'g', 'g', 'S').toLowerByteArray()
|
||||
"OggS".toByteArray(Charsets.UTF_8),
|
||||
),
|
||||
Pair(
|
||||
"audio/flac",
|
||||
charArrayOf('f', 'L', 'a', 'C').toLowerByteArray()
|
||||
)
|
||||
)
|
||||
"fLaC".toByteArray(Charsets.UTF_8),
|
||||
),
|
||||
Pair(
|
||||
"image/bmp",
|
||||
"BM".toByteArray(Charsets.UTF_8),
|
||||
),
|
||||
Pair(
|
||||
"image/webp",
|
||||
"RIFF****WEBP".toByteArray(Charsets.UTF_8),
|
||||
),
|
||||
).sortedByDescending { it.second.size }
|
||||
|
||||
private val sig3gp = arrayOf(
|
||||
"3ge6",
|
||||
|
@ -156,6 +164,25 @@ class AttachmentUploader(
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private const val wild = '?'.code.toByte()
|
||||
|
||||
private fun ByteArray.startWithWildcard(
|
||||
key: ByteArray,
|
||||
thisOffset: Int = 0,
|
||||
keyOffset: Int = 0,
|
||||
length: Int = key.size - keyOffset,
|
||||
): Boolean {
|
||||
if (thisOffset + length > this.size || keyOffset + length > key.size) {
|
||||
return false
|
||||
}
|
||||
for (i in 0 until length) {
|
||||
val cThis = this[i + thisOffset]
|
||||
val cKey = key[i + keyOffset]
|
||||
if (cKey != wild && cKey != cThis) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private val context = contextArg.applicationContext!!
|
||||
|
@ -173,12 +200,37 @@ class AttachmentUploader(
|
|||
channel = it
|
||||
launchIO {
|
||||
while (true) {
|
||||
try {
|
||||
handleRequest(it.receive())
|
||||
val request = try {
|
||||
it.receive()
|
||||
} catch (ex: Throwable) {
|
||||
when (ex) {
|
||||
is CancellationException, is ClosedReceiveChannelException -> break
|
||||
else -> context.showToast(ex)
|
||||
else -> {
|
||||
context.showToast(ex)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
val result = try {
|
||||
if (request.pa.isCancelled) continue
|
||||
withContext(request.pa.job + Dispatchers.IO) {
|
||||
request.upload()
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
TootApiResult(ex.withCaption("upload failed."))
|
||||
}
|
||||
try {
|
||||
request.pa.progress = ""
|
||||
withContext(Dispatchers.Main) {
|
||||
handleResult(request, result)
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
when (ex) {
|
||||
is CancellationException, is ClosedReceiveChannelException -> break
|
||||
else -> {
|
||||
context.showToast(ex)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,6 +251,9 @@ class AttachmentUploader(
|
|||
}
|
||||
|
||||
fun addRequest(request: AttachmentRequest) {
|
||||
|
||||
request.pa.progress = context.getString(R.string.attachment_handling_start)
|
||||
|
||||
// アップロード開始トースト(連発しない)
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastAttachmentAdd >= 5000L) {
|
||||
|
@ -212,23 +267,18 @@ class AttachmentUploader(
|
|||
launchIO { prepareChannel().send(request) }
|
||||
}
|
||||
|
||||
private suspend fun handleRequest(request: AttachmentRequest) {
|
||||
val result = request.upload()
|
||||
withContext(Dispatchers.Main) {
|
||||
handleResult(request, result)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private suspend fun AttachmentRequest.upload(): TootApiResult? {
|
||||
if (mimeType.isEmpty()) return TootApiResult("mime_type is empty.")
|
||||
|
||||
try {
|
||||
if (mimeType.isEmpty()) return TootApiResult("mime_type is empty.")
|
||||
|
||||
val client = TootApiClient(context, callback = object : TootApiCallback {
|
||||
override suspend fun isApiCancelled() = !coroutineContext.isActive
|
||||
})
|
||||
|
||||
client.account = account
|
||||
client.currentCallCallback = {}
|
||||
|
||||
val (ti, tiResult) = TootInstance.get(client)
|
||||
ti ?: return tiResult
|
||||
|
@ -246,18 +296,44 @@ class AttachmentUploader(
|
|||
)
|
||||
}
|
||||
}
|
||||
val mediaConfig = ti.configuration?.jsonObject("media_attachments")
|
||||
val imageResizeConfig = mediaConfig?.int("image_matrix_limit")
|
||||
?.takeIf { it > 0 }
|
||||
?.let { ResizeConfig(ResizeType.SquarePixel, it) }
|
||||
?: account.getResizeConfig()
|
||||
|
||||
// 設定からリサイズ指定を読む
|
||||
val resizeConfig = account.getResizeConfig()
|
||||
val movieResizeConfig = account.getMovieResizeConfig()
|
||||
|
||||
val opener = createOpener(uri, mimeType, resizeConfig)
|
||||
mediaConfig?.int("video_frame_rate_limit")
|
||||
?.takeIf { it >= 1f }
|
||||
?.let {
|
||||
movieResizeConfig.limitFrameRate = min(movieResizeConfig.limitFrameRate, it)
|
||||
}
|
||||
|
||||
mediaConfig?.int("video_matrix_limit")
|
||||
?.takeIf { it > 1 }
|
||||
?.let {
|
||||
movieResizeConfig.limitPixelMatrix = min(movieResizeConfig.limitPixelMatrix, it)
|
||||
}
|
||||
|
||||
// 入力データの変換など
|
||||
val opener = createOpener(
|
||||
uri,
|
||||
mimeType,
|
||||
imageResizeConfig = imageResizeConfig,
|
||||
movieResizeConfig = movieResizeConfig,
|
||||
pa,
|
||||
)
|
||||
|
||||
val mediaSizeMax = when {
|
||||
mimeType.startsWith("video") || mimeType.startsWith("audio") ->
|
||||
account.getMovieMaxBytes(ti)
|
||||
mediaConfig?.int("video_size_limit")
|
||||
?.takeIf { it > 0 }
|
||||
?: account.getMovieMaxBytes(ti)
|
||||
|
||||
else ->
|
||||
account.getImageMaxBytes(ti)
|
||||
else -> mediaConfig?.int("image_size_limit")
|
||||
?.takeIf { it > 0 }
|
||||
?: account.getImageMaxBytes(ti)
|
||||
}
|
||||
|
||||
val contentLength = getStreamSize(true, opener.open())
|
||||
|
@ -284,6 +360,20 @@ class AttachmentUploader(
|
|||
}
|
||||
|
||||
val fileName = fixDocumentName(getDocumentName(context.contentResolver, uri))
|
||||
pa.progress = context.getString(R.string.attachment_handling_uploading, 0)
|
||||
var nWrite = 0
|
||||
fun writeProgress(delta: Int) {
|
||||
nWrite += delta
|
||||
if (contentLength > 0) {
|
||||
val percent = (100f * nWrite.toFloat() / contentLength.toFloat()).toInt()
|
||||
if (percent < 100) {
|
||||
pa.progress =
|
||||
context.getString(R.string.attachment_handling_uploading, percent)
|
||||
} else {
|
||||
pa.progress = context.getString(R.string.attachment_handling_waiting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (account.isMisskey) {
|
||||
val multipartBuilder = MultipartBody.Builder()
|
||||
|
@ -314,6 +404,7 @@ class AttachmentUploader(
|
|||
while (true) {
|
||||
val r = inData.read(tmp, 0, tmp.size)
|
||||
if (r <= 0) break
|
||||
writeProgress(r)
|
||||
sink.write(tmp, 0, r)
|
||||
}
|
||||
}
|
||||
|
@ -325,9 +416,7 @@ class AttachmentUploader(
|
|||
"/api/drive/files/create",
|
||||
multipartBuilder.build().toPost()
|
||||
)
|
||||
|
||||
opener.deleteTempFile()
|
||||
onUploadEnd()
|
||||
|
||||
val jsonObject = result?.jsonObject
|
||||
if (jsonObject != null) {
|
||||
|
@ -364,6 +453,7 @@ class AttachmentUploader(
|
|||
while (true) {
|
||||
val r = inData.read(tmp, 0, tmp.size)
|
||||
if (r <= 0) break
|
||||
writeProgress(r)
|
||||
sink.write(tmp, 0, r)
|
||||
}
|
||||
}
|
||||
|
@ -390,6 +480,7 @@ class AttachmentUploader(
|
|||
// 202 accepted 以外はポーリングしない
|
||||
code != 202 -> return result
|
||||
}
|
||||
pa.progress = context.getString(R.string.attachment_handling_waiting2)
|
||||
|
||||
// ポーリングして処理完了を待つ
|
||||
val id =
|
||||
|
@ -422,7 +513,6 @@ class AttachmentUploader(
|
|||
|
||||
val result = postV2()
|
||||
opener.deleteTempFile()
|
||||
onUploadEnd()
|
||||
|
||||
val jsonObject = result?.jsonObject
|
||||
if (jsonObject != null) {
|
||||
|
@ -444,10 +534,14 @@ class AttachmentUploader(
|
|||
pa.status = when (pa.attachment) {
|
||||
null -> {
|
||||
if (result != null) {
|
||||
context.showToast(
|
||||
true,
|
||||
"${result.error} ${result.response?.request?.method} ${result.response?.request?.url}"
|
||||
)
|
||||
when {
|
||||
// キャンセルはトーストを出さない
|
||||
result.error?.contains("cancel", ignoreCase = true) == true -> Unit
|
||||
else -> context.showToast(
|
||||
true,
|
||||
"${result.error} ${result.response?.request?.method} ${result.response?.request?.url}"
|
||||
)
|
||||
}
|
||||
}
|
||||
PostAttachment.Status.Error
|
||||
}
|
||||
|
@ -475,77 +569,51 @@ class AttachmentUploader(
|
|||
fun deleteTempFile()
|
||||
}
|
||||
|
||||
private fun createOpener(
|
||||
private suspend fun createOpener(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
resizeConfig: ResizeConfig,
|
||||
imageResizeConfig: ResizeConfig,
|
||||
movieResizeConfig: MovieResizeConfig? = null,
|
||||
postAttachment: PostAttachment? = null,
|
||||
): InputStreamOpener {
|
||||
|
||||
while (true) {
|
||||
if (mimeType == MIME_TYPE_JPEG || mimeType == MIME_TYPE_PNG) {
|
||||
// 静止画(リサイズできなくてもOK)
|
||||
try {
|
||||
// 画像の種別
|
||||
val isJpeg = MIME_TYPE_JPEG == mimeType
|
||||
val isPng = MIME_TYPE_PNG == mimeType
|
||||
if (!isJpeg && !isPng) {
|
||||
log.d("createOpener: source is not jpeg or png")
|
||||
break
|
||||
}
|
||||
|
||||
val bitmap = createResizedBitmap(
|
||||
context,
|
||||
return createResizedImageOpener(
|
||||
uri,
|
||||
resizeConfig,
|
||||
skipIfNoNeedToResizeAndRotate = true
|
||||
mimeType,
|
||||
imageResizeConfig,
|
||||
postAttachment,
|
||||
)
|
||||
if (bitmap != null) {
|
||||
try {
|
||||
val cacheDir = context.externalCacheDir
|
||||
if (cacheDir == null) {
|
||||
context.showToast(false, "getExternalCacheDir returns null.")
|
||||
break
|
||||
}
|
||||
|
||||
cacheDir.mkdir()
|
||||
|
||||
val tempFile = File(cacheDir, "tmp." + Thread.currentThread().id)
|
||||
FileOutputStream(tempFile).use { os ->
|
||||
if (isJpeg) {
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, os)
|
||||
} else {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
|
||||
}
|
||||
}
|
||||
|
||||
return object : InputStreamOpener {
|
||||
|
||||
override val mimeType: String
|
||||
get() = mimeType
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun open(): InputStream {
|
||||
return FileInputStream(tempFile)
|
||||
}
|
||||
|
||||
override fun deleteTempFile() {
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
context.showToast(ex, "Resizing image failed.")
|
||||
log.w(ex, "createResizedImageOpener failed. fall back to original image.")
|
||||
}
|
||||
} else if (mimeType.startsWith("image/")) {
|
||||
// 静止画(変換必須)
|
||||
// 例外を投げるかもしれない
|
||||
return createResizedImageOpener(
|
||||
uri,
|
||||
mimeType,
|
||||
imageResizeConfig,
|
||||
postAttachment,
|
||||
forcePng = true
|
||||
)
|
||||
} else {
|
||||
// 動画画(リサイズできなくてもOK)
|
||||
try {
|
||||
return createResizedMovieOpener(
|
||||
uri,
|
||||
mimeType,
|
||||
movieResizeConfig,
|
||||
postAttachment,
|
||||
)
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "createResizedMovieOpener failed. fall back to original movie.")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return object : InputStreamOpener {
|
||||
|
||||
override val mimeType: String
|
||||
get() = mimeType
|
||||
override val mimeType = mimeType
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun open(): InputStream {
|
||||
|
@ -558,6 +626,101 @@ class AttachmentUploader(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createResizedImageOpener(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
imageResizeConfig: ResizeConfig,
|
||||
postAttachment: PostAttachment? = null,
|
||||
forcePng: Boolean = false,
|
||||
): InputStreamOpener {
|
||||
val cacheDir = context.externalCacheDir
|
||||
?.apply { mkdirs() }
|
||||
?: error("getExternalCacheDir returns null.")
|
||||
|
||||
val outputMimeType = if (forcePng || mimeType == MIME_TYPE_PNG) {
|
||||
MIME_TYPE_PNG
|
||||
} else {
|
||||
MIME_TYPE_JPEG
|
||||
}
|
||||
|
||||
val tempFile = File(cacheDir, "tmp." + Thread.currentThread().id)
|
||||
val bitmap = createResizedBitmap(
|
||||
context,
|
||||
uri,
|
||||
imageResizeConfig,
|
||||
skipIfNoNeedToResizeAndRotate = !forcePng
|
||||
) ?: error("createResizedBitmap returns null.")
|
||||
postAttachment?.progress = context.getString(R.string.attachment_handling_compress)
|
||||
try {
|
||||
FileOutputStream(tempFile).use { os ->
|
||||
if (outputMimeType == MIME_TYPE_PNG) {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
|
||||
} else {
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, os)
|
||||
}
|
||||
}
|
||||
return createTempFileOpener(outputMimeType, tempFile)
|
||||
} finally {
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun createResizedMovieOpener(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
movieResizeConfig: MovieResizeConfig? = null,
|
||||
postAttachment: PostAttachment? = null,
|
||||
): InputStreamOpener {
|
||||
movieResizeConfig ?: error("missing movieResizeConfig.")
|
||||
|
||||
val cacheDir = context.externalCacheDir
|
||||
?.apply { mkdirs() }
|
||||
?: error("getExternalCacheDir returns null.")
|
||||
|
||||
val tempFile = File(cacheDir, "movie." + Thread.currentThread().id + ".tmp")
|
||||
val outFile = File(cacheDir, "movie." + Thread.currentThread().id + ".mp4")
|
||||
// 入力ファイルをコピーする
|
||||
(context.contentResolver.openInputStream(uri)
|
||||
?: error("openInputStream returns null.")).use { inStream ->
|
||||
FileOutputStream(tempFile).use { inStream.copyTo(it) }
|
||||
}
|
||||
var resultFile: File? = null
|
||||
try {
|
||||
val result = transcodeVideo(
|
||||
tempFile,
|
||||
outFile,
|
||||
movieResizeConfig,
|
||||
) {
|
||||
val percent = (it * 100f).toInt()
|
||||
postAttachment?.progress =
|
||||
context.getString(R.string.attachment_handling_compress_ratio, percent)
|
||||
}
|
||||
resultFile = result
|
||||
return createTempFileOpener(
|
||||
when (result) {
|
||||
tempFile -> mimeType
|
||||
else -> "video/mp4"
|
||||
},
|
||||
result
|
||||
)
|
||||
} finally {
|
||||
if (outFile != resultFile) outFile.delete()
|
||||
if (tempFile != resultFile) tempFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTempFileOpener(mimeType: String, file: File) =
|
||||
object : InputStreamOpener {
|
||||
override val mimeType = mimeType
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun open() = FileInputStream(file)
|
||||
override fun deleteTempFile() {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun getMimeType(uri: Uri, mimeTypeArg: String?): String? {
|
||||
// image/j()pg だの image/j(e)pg だの、mime type を誤記するアプリがあまりに多い
|
||||
// クレームで消耗するのを減らすためにファイルヘッダを確認する
|
||||
|
@ -593,7 +756,7 @@ class AttachmentUploader(
|
|||
for (pair in imageHeaderList) {
|
||||
val type = pair.first
|
||||
val header = pair.second
|
||||
if (nRead >= header.size && data.startWith(header)) return type
|
||||
if (nRead >= header.size && data.startWithWildcard(header)) return type
|
||||
}
|
||||
|
||||
// scan frame header
|
||||
|
@ -783,7 +946,11 @@ class AttachmentUploader(
|
|||
return Pair(result, resultAttachment)
|
||||
}
|
||||
|
||||
fun isAcceptableMimeType(instance: TootInstance?, mimeType: String, isReply: Boolean): Boolean {
|
||||
fun isAcceptableMimeType(
|
||||
instance: TootInstance?,
|
||||
mimeType: String,
|
||||
isReply: Boolean,
|
||||
): Boolean {
|
||||
if (instance?.instanceType == InstanceType.Pixelfed) {
|
||||
if (isReply) {
|
||||
context.showToast(true, R.string.pixelfed_does_not_allow_reply_with_media)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import jp.juggler.subwaytooter.api.entity.TootAttachment
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
class PostAttachment : Comparable<PostAttachment> {
|
||||
interface Callback {
|
||||
fun onPostAttachmentComplete(pa: PostAttachment)
|
||||
fun onPostAttachmentProgress()
|
||||
}
|
||||
|
||||
enum class Status(val id: Int) {
|
||||
|
@ -13,9 +15,18 @@ class PostAttachment : Comparable<PostAttachment> {
|
|||
Error(3),
|
||||
}
|
||||
|
||||
var isCancelled = false
|
||||
val job = Job()
|
||||
var status: Status
|
||||
var attachment: TootAttachment? = null
|
||||
var callback: Callback? = null
|
||||
var progress =""
|
||||
set(value){
|
||||
if( field!=value){
|
||||
field = value
|
||||
callback?.onPostAttachmentProgress()
|
||||
}
|
||||
}
|
||||
|
||||
constructor(callback: Callback) {
|
||||
this.status = Status.Progress
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package jp.juggler.util
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class JsonProperty<ValueType>(
|
||||
val src: JsonObject,
|
||||
val key: String,
|
||||
val defVal: ValueType,
|
||||
)
|
||||
|
||||
operator fun JsonProperty<String>.getValue(thisRef: Any?, property: KProperty<*>): String {
|
||||
return src.string(key) ?: defVal
|
||||
}
|
||||
|
||||
operator fun JsonProperty<String>.setValue(thisRef: Any?, property: KProperty<*>, value: String) {
|
||||
src[key] = value
|
||||
}
|
||||
|
||||
operator fun JsonProperty<Int>.getValue(thisRef: Any?, property: KProperty<*>): Int {
|
||||
return src.int(key) ?: defVal
|
||||
}
|
||||
|
||||
operator fun JsonProperty<Int>.setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
|
||||
src[key] = value
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package jp.juggler.util
|
||||
|
||||
import com.otaliastudios.transcoder.Transcoder
|
||||
import com.otaliastudios.transcoder.TranscoderListener
|
||||
import com.otaliastudios.transcoder.common.Size
|
||||
import com.otaliastudios.transcoder.resize.Resizer
|
||||
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
|
||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||
import jp.juggler.util.VideoInfo.Companion.videoInfo
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.sqrt
|
||||
|
||||
private val log = LogCategory("MovieUtils")
|
||||
|
||||
data class MovieResizeConfig(
|
||||
var mode: Int = 0,
|
||||
var limitFrameRate: Int = 30,
|
||||
var limitBitrate: Long = 2_000_000L,
|
||||
var limitPixelMatrix: Int = 2304000,
|
||||
) {
|
||||
companion object {
|
||||
const val MODE_AUTO = 0
|
||||
const val MODE_NO = 1
|
||||
const val NODE_ALWAYS = 2
|
||||
}
|
||||
}
|
||||
|
||||
class AtMostSquarePixelResizer(private val limit: Int) : Resizer {
|
||||
override fun getOutputSize(inputSize: Size): Size {
|
||||
val inSquarePixel = abs(inputSize.major) * abs(inputSize.minor)
|
||||
if (inSquarePixel <= limit || inputSize.major <= 0 || inputSize.minor <= 0) {
|
||||
return inputSize
|
||||
}
|
||||
val aspect = inputSize.major.toFloat() / inputSize.minor.toFloat()
|
||||
return Size(
|
||||
max(1, (sqrt(limit.toFloat() * aspect) + 0.5f).toInt()),
|
||||
max(1, (sqrt(limit.toFloat() / aspect) + 0.5f).toInt()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun transcodeVideo(
|
||||
inFile: File,
|
||||
outFile: File,
|
||||
resizeConfig: MovieResizeConfig,
|
||||
onProgress: (Float) -> Unit,
|
||||
): File = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
when (resizeConfig.mode) {
|
||||
MovieResizeConfig.MODE_NO ->
|
||||
return@withContext inFile
|
||||
MovieResizeConfig.MODE_AUTO -> {
|
||||
val info = inFile.videoInfo
|
||||
if (info.size.w * info.size.h <= resizeConfig.limitPixelMatrix &&
|
||||
(info.actualBps ?: 0) <= resizeConfig.limitBitrate &&
|
||||
(info.frameRatio?.toInt() ?: 0) <= resizeConfig.limitFrameRate
|
||||
) {
|
||||
log.i("transcodeVideo skip.")
|
||||
return@withContext inFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val resultFile = FileInputStream(inFile).use { inStream ->
|
||||
// 進捗コールバックの発生頻度が多すぎるので間引く
|
||||
val progressChannel = Channel<Float>(capacity = Channel.CONFLATED)
|
||||
val progressSender = launch(Dispatchers.Main) {
|
||||
try {
|
||||
while (true) {
|
||||
onProgress(progressChannel.receive())
|
||||
delay(1000L)
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
when (ex) {
|
||||
is ClosedReceiveChannelException -> log.i("progress closed.")
|
||||
is CancellationException -> log.i("progress cancelled.")
|
||||
else -> log.w(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
suspendCancellableCoroutine<File> { cont ->
|
||||
// https://github.com/natario1/Transcoder/pull/160
|
||||
// ワークアラウンドとしてファイルではなくfdを渡す
|
||||
val future = Transcoder.into(outFile.canonicalPath)
|
||||
.addDataSource(inStream.fd)
|
||||
.setVideoTrackStrategy(
|
||||
DefaultVideoStrategy.Builder()
|
||||
.addResizer(
|
||||
AtMostSquarePixelResizer(resizeConfig.limitPixelMatrix)
|
||||
)
|
||||
.frameRate(resizeConfig.limitFrameRate)
|
||||
.keyFrameInterval(10f)
|
||||
.bitRate(resizeConfig.limitBitrate)
|
||||
.build()
|
||||
)
|
||||
.setAudioTrackStrategy(
|
||||
DefaultAudioStrategy.Builder()
|
||||
.channels(2)
|
||||
.sampleRate(44100)
|
||||
.bitRate(96_000L)
|
||||
.build()
|
||||
)
|
||||
.setListener(object : TranscoderListener {
|
||||
override fun onTranscodeCanceled() {
|
||||
log.w("onTranscodeCanceled")
|
||||
cont.resumeWithException(CancellationException("transcode cancelled."))
|
||||
}
|
||||
|
||||
override fun onTranscodeFailed(exception: Throwable) {
|
||||
log.w("onTranscodeFailed")
|
||||
cont.resumeWithException(exception)
|
||||
}
|
||||
|
||||
override fun onTranscodeCompleted(successCode: Int) {
|
||||
when (successCode) {
|
||||
Transcoder.SUCCESS_TRANSCODED -> outFile
|
||||
/* Transcoder.SUCCESS_NOT_NEEDED */ else -> inFile
|
||||
}.let { cont.resumeWith(Result.success(it)) }
|
||||
}
|
||||
|
||||
override fun onTranscodeProgress(progress: Double) {
|
||||
val result = progressChannel.trySend(progress.toFloat())
|
||||
if (!result.isSuccess) {
|
||||
log.w("trySend $result")
|
||||
}
|
||||
}
|
||||
}).transcode()
|
||||
cont.invokeOnCancellation { future.cancel(true) }
|
||||
}
|
||||
} finally {
|
||||
progressChannel.close()
|
||||
progressSender.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
resultFile
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.w("delete outFile due to error.")
|
||||
outFile.delete()
|
||||
throw ex
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
package jp.juggler.util
|
||||
|
||||
import android.media.MediaCodecList
|
||||
import android.media.MediaFormat
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.os.Build
|
||||
import java.io.File
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* 動画の情報
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class VideoInfo(
|
||||
val file: File,
|
||||
mmr: MediaMetadataRetriever,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("VideoInfo")
|
||||
|
||||
val File.videoInfo: VideoInfo
|
||||
get() = MediaMetadataRetriever().use { mmr ->
|
||||
mmr.setDataSource(canonicalPath)
|
||||
VideoInfo(this, mmr)
|
||||
}
|
||||
|
||||
private fun MediaMetadataRetriever.string(key: Int) =
|
||||
extractMetadata(key)
|
||||
|
||||
private fun MediaMetadataRetriever.int(key: Int) =
|
||||
string(key)?.toIntOrNull()
|
||||
|
||||
private fun MediaMetadataRetriever.long(key: Int) =
|
||||
string(key)?.toLongOrNull()
|
||||
|
||||
|
||||
/**
|
||||
* 調査のためコーデックを列挙して情報をログに出す
|
||||
*/
|
||||
fun dumpCodec() {
|
||||
val mcl = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||
for (info in mcl.codecInfos) {
|
||||
try {
|
||||
if (!info.isEncoder) continue
|
||||
val caps = try {
|
||||
info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC) ?: continue
|
||||
} catch (ex: Throwable) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (colorFormat in caps.colorFormats) {
|
||||
log.i("${info.name} color 0x${colorFormat.toString(16)}")
|
||||
// OMX.qcom.video.encoder.avc color 7fa30c04 不明
|
||||
// OMX.qcom.video.encoder.avc color 7f000789 COLOR_FormatSurface
|
||||
// OMX.qcom.video.encoder.avc color 7f420888 COLOR_FormatYUV420Flexible
|
||||
// OMX.qcom.video.encoder.avc color 15 COLOR_Format32bitBGRA8888
|
||||
}
|
||||
caps.videoCapabilities.bitrateRange?.let { range ->
|
||||
log.i("bitrateRange $range")
|
||||
}
|
||||
caps.videoCapabilities.supportedFrameRates?.let { range ->
|
||||
log.i("supportedFrameRates $range")
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
caps.encoderCapabilities.qualityRange?.let { range ->
|
||||
log.i("qualityRange $range")
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex)
|
||||
// type is not supported
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Size(var w: Int, var h: Int) {
|
||||
override fun toString() = "[$w,$h]"
|
||||
|
||||
private val aspect: Float get() = w.toFloat() / h.toFloat()
|
||||
|
||||
/**
|
||||
* アスペクト比を維持しつつ上限に合わせた解像度を提案する
|
||||
* - 拡大はしない
|
||||
*/
|
||||
fun scaleTo(limitLonger: Int, limitShorter: Int): Size {
|
||||
val inSize = this
|
||||
// ゼロ除算対策
|
||||
if (inSize.w < 1 || inSize.h < 1) {
|
||||
return Size(limitLonger, limitShorter)
|
||||
}
|
||||
val inAspect = inSize.aspect
|
||||
// 入力の縦横に合わせて上限を決める
|
||||
val outSize = if (inAspect >= 1f) {
|
||||
Size(limitLonger, limitShorter)
|
||||
} else {
|
||||
Size(limitShorter, limitLonger)
|
||||
}
|
||||
// 縦横比を比較する
|
||||
return if (inAspect >= outSize.aspect) {
|
||||
// 入力のほうが横長なら横幅基準でスケーリングする
|
||||
// 拡大はしない
|
||||
val scale = outSize.w.toFloat() / inSize.w.toFloat()
|
||||
if (scale >= 1f) inSize else outSize.apply {
|
||||
h = min(h, (scale * inSize.h + 0.5f).toInt())
|
||||
}
|
||||
} else {
|
||||
// 入力のほうが縦長なら縦幅基準でスケーリングする
|
||||
// 拡大はしない
|
||||
val scale = outSize.h.toFloat() / inSize.h.toFloat()
|
||||
if (scale >= 1f) inSize else outSize.apply {
|
||||
w = min(w, (scale * inSize.w + 0.5f).toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val mimeType = mmr.string(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
|
||||
|
||||
val rotation = mmr.int(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) ?: 0
|
||||
|
||||
val size = Size(
|
||||
mmr.int(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) ?: 0,
|
||||
mmr.int(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) ?: 0,
|
||||
)
|
||||
|
||||
val bitrate = mmr.int(MediaMetadataRetriever.METADATA_KEY_BITRATE)
|
||||
|
||||
val duration = mmr.long(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
?.toFloat()?.div(1000)?.takeIf { it > 0.1f }
|
||||
|
||||
val frameCount = if (Build.VERSION.SDK_INT >= 28) {
|
||||
mmr.int(MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT)?.takeIf { it > 0 }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val frameRatio = if (frameCount != null && duration != null) {
|
||||
frameCount.toFloat().div(duration)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val audioSampleRate = if (Build.VERSION.SDK_INT >= 31) {
|
||||
mmr.int(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE)?.takeIf { it > 0 }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val actualBps by lazy {
|
||||
val fileSize = file.length()
|
||||
|
||||
// ファイルサイズを取得できないならエラー
|
||||
if (fileSize <= 0L) return@lazy null
|
||||
|
||||
// 時間帳が短すぎるなら算出できない
|
||||
if (duration == null || duration < 0.1f) return@lazy null
|
||||
|
||||
// bpsを計算
|
||||
fileSize.toFloat().div(duration).times(8).toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* 動画のファイルサイズが十分に小さいなら真
|
||||
*/
|
||||
fun isSmallEnough(limitBps: Int): Boolean {
|
||||
val fileSize = file.length()
|
||||
// ファイルサイズを取得できないならエラー
|
||||
if (fileSize <= 0L) error("too small file. ${file.canonicalPath}")
|
||||
// ファイルサイズが500KB以内ならビットレートを気にしない
|
||||
if (fileSize < 500_000) return true
|
||||
|
||||
// ファイルサイズからビットレートを計算できなかったなら再エンコード必要
|
||||
val actualBps = this.actualBps ?: return false
|
||||
|
||||
// bpsを計算
|
||||
log.i("isSmallEnough duration=$duration, bps=$actualBps/$limitBps")
|
||||
return actualBps <= limitBps
|
||||
}
|
||||
|
||||
override fun toString() =
|
||||
"rotation=$rotation, size=$size, frameRatio=$frameRatio, bitrate=${actualBps ?: bitrate}, audioSampleRate=$audioSampleRate, mimeType=$mimeType, file=${file.canonicalPath}"
|
||||
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/svContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="128dp"
|
||||
android:paddingTop="12dp"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
tools:ignore="TooManyViews,Autofill">
|
||||
|
||||
|
@ -565,8 +565,7 @@
|
|||
style="@style/setting_row_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="40dp"
|
||||
/>
|
||||
android:minHeight="40dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPushSubscription"
|
||||
|
@ -659,6 +658,10 @@
|
|||
android:labelFor="@+id/etMediaSizeMax"
|
||||
android:text="@string/media_attachment_max_byte_size" />
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:text="@string/option_deprecated_mastodon342" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etMediaSizeMax"
|
||||
style="@style/setting_edit_text"
|
||||
|
@ -667,6 +670,23 @@
|
|||
android:gravity="center"
|
||||
android:inputType="number" />
|
||||
|
||||
|
||||
<View style="@style/setting_divider" />
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:text="@string/resize_image" />
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:text="@string/option_deprecated_mastodon342" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spResizeImage"
|
||||
style="@style/setting_row_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<View style="@style/setting_divider" />
|
||||
|
||||
<TextView
|
||||
|
@ -674,6 +694,10 @@
|
|||
android:labelFor="@+id/etMovieSizeMax"
|
||||
android:text="@string/media_attachment_max_byte_size_movie" />
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:text="@string/option_deprecated_mastodon342" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etMovieSizeMax"
|
||||
style="@style/setting_edit_text"
|
||||
|
@ -686,15 +710,54 @@
|
|||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:text="@string/resize_image" />
|
||||
android:text="@string/movie_transcode" />
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label_indent1"
|
||||
android:text="@string/movie_transcode_mode" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spResizeImage"
|
||||
style="@style/setting_row_button"
|
||||
android:id="@+id/spMovieTranscodeMode"
|
||||
style="@style/setting_spinner_indent1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="40dp" />
|
||||
|
||||
<View style="@style/setting_divider" />
|
||||
<TextView
|
||||
style="@style/setting_row_label_indent1"
|
||||
android:text="@string/movie_transcode_max_bitrate" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etMovieBitrate"
|
||||
style="@style/setting_edit_text_indent1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:inputType="number" />
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label_indent1"
|
||||
android:text="@string/movie_transcode_max_frame_rate" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etMovieFrameRate"
|
||||
style="@style/setting_edit_text_indent1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:inputType="numberDecimal" />
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label_indent1"
|
||||
android:text="@string/movie_transcode_max_square_pixels" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etMovieSquarePixels"
|
||||
style="@style/setting_edit_text_indent1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:inputType="number" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
|
@ -2,7 +2,6 @@
|
|||
<jp.juggler.subwaytooter.actpost.ActPostRootLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/viewRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
@ -113,13 +112,13 @@
|
|||
android:layout_height="48dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:gravity="top|start"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||
android:id="@+id/ivMedia1"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
@ -127,7 +126,7 @@
|
|||
<jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||
android:id="@+id/ivMedia2"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
@ -135,7 +134,7 @@
|
|||
<jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||
android:id="@+id/ivMedia3"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
@ -143,10 +142,20 @@
|
|||
<jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||
android:id="@+id/ivMedia4"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:scaleType="fitCenter" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/tvAttachmentProgress"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:textSize="11sp"
|
||||
tools:text="アップロード中です"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
|
@ -285,7 +294,7 @@
|
|||
</LinearLayout>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spEnquete"
|
||||
android:id="@+id/spPollType"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
|
|
|
@ -1108,4 +1108,18 @@
|
|||
<string name="url_of_user_or_status">ユーザや投稿のURL</string>
|
||||
<string name="unbookmark">ブックマーク解除</string>
|
||||
<string name="event_text_alpha">一部の通知の本文アルファ値(0.0~1.0, デフォルト:1。アプリ再起動が必要)</string>
|
||||
<string name="attachment_handling_start">初期化中…</string>
|
||||
<string name="attachment_handling_compress">圧縮中…</string>
|
||||
<string name="attachment_handling_compress_ratio">圧縮中 %1$d%%…</string>
|
||||
<string name="attachment_handling_uploading">アップロード中 %1$d%%…</string>
|
||||
<string name="attachment_handling_waiting">応答待ち…</string>
|
||||
<string name="attachment_handling_waiting2">応答待ち(非同期)…</string>
|
||||
<string name="option_deprecated_mastodon342">(Mastodon 3.4.2以降では指定した値ではなく、サーバから提供される情報が使われます)</string>
|
||||
<string name="movie_transcode">動画の再圧縮</string>
|
||||
<string name="movie_transcode_mode">モード</string>
|
||||
<string name="movie_transcode_max_bitrate">最大ビットレート(単位:秒間ビット数。デフォルト=2,000,000。サーバからの指定でより少なくなる場合があります。)</string>
|
||||
<string name="movie_transcode_max_frame_rate">最大フレームレート(単位:秒間フレーム数。デフォルト=30。サーバからの指定でより少なくなる場合があります。)</string>
|
||||
<string name="movie_transcode_max_square_pixels">最大平方ピクセル数(デフォルト=2304000)</string>
|
||||
<string name="always">常に</string>
|
||||
<string name="auto">自動</string>
|
||||
</resources>
|
||||
|
|
|
@ -1118,5 +1118,19 @@
|
|||
<string name="url_of_user_or_status">URL of user or status</string>
|
||||
<string name="url_parse_failed">parse error.</string>
|
||||
<string name="unbookmark">Unbookmark</string>
|
||||
<string name="event_text_alpha">Text alpha for some notifications (0.0~1.0, default:1. app restart required)</string>
|
||||
<string name="event_text_alpha">Text alpha for some notifications (0.0~1.0, default=1. app restart required)</string>
|
||||
<string name="attachment_handling_start">Initializing…</string>
|
||||
<string name="attachment_handling_compress">Compressing…</string>
|
||||
<string name="attachment_handling_compress_ratio">Compressing %1$d%%…</string>
|
||||
<string name="attachment_handling_uploading">Uploading %1$d%%…</string>
|
||||
<string name="attachment_handling_waiting">Waiting response…</string>
|
||||
<string name="attachment_handling_waiting2">Waiting response(asynchronized)…</string>
|
||||
<string name="option_deprecated_mastodon342">(This option is deprecated for Mastodon 3.4.2+. App uses information from the server.)</string>
|
||||
<string name="movie_transcode">Movie transcoding</string>
|
||||
<string name="movie_transcode_mode">Mode</string>
|
||||
<string name="movie_transcode_max_bitrate">Max bitrate (Unit:bits per second. default=2,000,000. Also it may be reduced by server configuration.)</string>
|
||||
<string name="movie_transcode_max_frame_rate">Max frame rate(Unit:frames per second. default=30. Also it may be reduced by server configuration.)</string>
|
||||
<string name="movie_transcode_max_square_pixels">Max square pixels(default=2304000)</string>
|
||||
<string name="always">Always</string>
|
||||
<string name="auto">Auto</string>
|
||||
</resources>
|
||||
|
|
|
@ -230,6 +230,25 @@
|
|||
<item name="android:imeOptions">actionDone</item>
|
||||
</style>
|
||||
|
||||
<style name="setting_edit_text_indent1" parent="@style/setting_horizontal_stretch">
|
||||
<item name="android:imeOptions">actionDone</item>
|
||||
|
||||
<item name="android:layout_marginLeft">48dp</item>
|
||||
<item name="android:layout_marginRight">0dp</item>
|
||||
|
||||
<item name="android:layout_marginStart" tools:ignore="NewApi">48dp</item>
|
||||
<item name="android:layout_marginEnd" tools:ignore="NewApi">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="setting_spinner_indent1" parent="@style/setting_horizontal_stretch">
|
||||
|
||||
<item name="android:layout_marginLeft">48dp</item>
|
||||
<item name="android:layout_marginRight">0dp</item>
|
||||
|
||||
<item name="android:layout_marginStart" tools:ignore="NewApi">48dp</item>
|
||||
<item name="android:layout_marginEnd" tools:ignore="NewApi">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="recycler_view_with_scroll_bar">
|
||||
<item name="android:clipToPadding">false</item>
|
||||
<item name="android:fadeScrollbars">false</item>
|
||||
|
|
|
@ -127,8 +127,8 @@ for my $lang ( sort keys %langs ){
|
|||
# 残りの部分に%が登場したらエラー
|
||||
my $sv = $value;
|
||||
$sv =~ s/(%\d+\$[\d\.]*[sdxf])//g;
|
||||
# Unit:%. を除外したい
|
||||
$sv =~ s/%[\s.。]//g;
|
||||
# Unit:%. や %% を除外したい
|
||||
$sv =~ s/%[\s.。%]//g;
|
||||
if( $sv =~ /%/ ){
|
||||
$hasError =1;
|
||||
print "!! ($lang)$name : broken param: $sv // $value\n";
|
||||
|
|
Loading…
Reference in New Issue