diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a5a013a9..fed64a39 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -256,6 +256,8 @@
+
+
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAppSettingChild.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAppSettingChild.kt
index 542d6323..a4a6ef6c 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ActAppSettingChild.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ActAppSettingChild.kt
@@ -1,5 +1,6 @@
package jp.juggler.subwaytooter
+import android.app.PendingIntent
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.ColorStateList
@@ -27,6 +28,7 @@ import org.jetbrains.anko.backgroundDrawable
import org.jetbrains.anko.textColor
import java.io.File
import java.io.FileOutputStream
+import java.lang.ref.WeakReference
import java.text.NumberFormat
import java.util.*
import java.util.concurrent.TimeUnit
@@ -157,6 +159,7 @@ class ActAppSettingChild : AppCompatActivity()
private var etRoundRatio : EditText? = null
private var etBoostAlpha : EditText? = null
private var etMediaReadTimeout : EditText? = null
+ private var etTranslateAppComponent : EditText? = null
private var tvTimelineFontUrl : TextView? = null
private var timeline_font : String? = null
@@ -201,6 +204,13 @@ class ActAppSettingChild : AppCompatActivity()
private var hasLinkColorUi = false
private var hasColumnColorDefaultUi = false
+ override fun onResume() {
+ super.onResume()
+
+ checkIntentChoiced()
+ }
+
+
override fun onPause() {
super.onPause()
@@ -378,6 +388,8 @@ class ActAppSettingChild : AppCompatActivity()
, R.id.btnBackgroundColorVotedReset
, R.id.btnBackgroundColorFollowRequestedEdit
, R.id.btnBackgroundColorFollowRequestedReset
+ , R.id.btnTranslateAppComponentEdit
+ , R.id.btnTranslateAppComponentReset
).forEach {
findViewById(it)?.setOnClickListener(this)
}
@@ -432,6 +444,9 @@ class ActAppSettingChild : AppCompatActivity()
etMediaReadTimeout = findViewById(R.id.etMediaReadTimeout)
etMediaReadTimeout?.addTextChangedListener(this)
+ etTranslateAppComponent = findViewById(R.id.etTranslateAppComponent)
+ etTranslateAppComponent?.addTextChangedListener(this)
+
tvTimelineFontSize = findViewById(R.id.tvTimelineFontSize)
tvAcctFontSize = findViewById(R.id.tvAcctFontSize)
tvNotificationTlFontSize = findViewById(R.id.tvNotificationTlFontSize)
@@ -587,6 +602,7 @@ class ActAppSettingChild : AppCompatActivity()
etBoostAlpha?.setText(Pref.spBoostAlpha(pref))
etMediaReadTimeout?.setText(Pref.spMediaReadTimeout(pref))
+ etTranslateAppComponent?.setText(Pref.spTranslateAppComponent(pref))
timeline_font = Pref.spTimelineFont(pref)
timeline_font_bold = Pref.spTimelineFontBold(pref)
@@ -674,6 +690,7 @@ class ActAppSettingChild : AppCompatActivity()
putText(Pref.spRoundRatio, etRoundRatio)
putText(Pref.spBoostAlpha, etBoostAlpha)
putText(Pref.spMediaReadTimeout, etMediaReadTimeout)
+ putText(Pref.spTranslateAppComponent,etTranslateAppComponent)
fun putIf(hasUi : Boolean, sp : StringPref, value : String) {
if(! hasUi) return
@@ -1114,6 +1131,54 @@ class ActAppSettingChild : AppCompatActivity()
saveUIToData()
}
+ R.id.btnTranslateAppComponentEdit -> {
+
+ val intent = Intent()
+ intent.action = Intent.ACTION_SEND
+ intent.type = "text/plain"
+ intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.content_sample))
+
+ // このifはwhenにしてはならない。APIバージョン関連の警告が出てしまう
+ @Suppress("CascadeIf")
+ if(intent.resolveActivity(packageManager) == null) {
+ // ACTION_SENDを受け取れるアプリがインストールされてない
+ showToast(this, true, getString(R.string.missing_app_can_receive_action_send))
+ } else if(Build.VERSION.SDK_INT <= 21) {
+ // createChooserにIntentSenderを指定できるのはAndroid 22以降
+ showToast(
+ this,
+ true,
+ getString(R.string.translation_app_chooser_works_android_5_1)
+ )
+ } else try {
+ ChooseReceiver.lastComponentName = null
+ ChooseReceiver.setCallback{ checkIntentChoiced() }
+
+ val receiver = Intent(this, ChooseReceiver::class.java)
+ val pendingIntent = PendingIntent.getBroadcast(
+ this,
+ 1,
+ receiver,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ startActivity(
+ Intent.createChooser(
+ intent,
+ getString(R.string.select_translate_app),
+ pendingIntent.intentSender
+ )
+ )
+
+ } catch(ex : Throwable) {
+ log.trace(ex)
+ showToast(this, ex, "btnTranslateAppComponentEdit failed.")
+ }
+ }
+
+ R.id.btnTranslateAppComponentReset -> {
+ etTranslateAppComponent?.setText("")
+ saveUIToData()
+ }
}
}
@@ -1642,4 +1707,16 @@ class ActAppSettingChild : AppCompatActivity()
return list[position].id
}
}
+
+ private fun checkIntentChoiced(){
+ if( isDestroyed ) return
+
+ val cn = ChooseReceiver.lastComponentName
+ if(cn != null && etTranslateAppComponent != null) {
+ etTranslateAppComponent?.setText("${cn.packageName}/${cn.className}")
+ saveUIToData()
+ ChooseReceiver.lastComponentName = null
+ }
+ }
+
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActText.kt b/app/src/main/java/jp/juggler/subwaytooter/ActText.kt
index 6d804782..0b570077 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ActText.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ActText.kt
@@ -1,22 +1,21 @@
package jp.juggler.subwaytooter
import android.app.SearchManager
-import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
-import jp.juggler.subwaytooter.api.entity.*
+import jp.juggler.subwaytooter.api.entity.TootAccount
+import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.table.MutedWord
import jp.juggler.subwaytooter.table.SavedAccount
-import jp.juggler.subwaytooter.util.DecodeOptions
+import jp.juggler.subwaytooter.util.TootTextEncoder
import jp.juggler.util.LogCategory
import jp.juggler.util.copyToClipboard
import jp.juggler.util.hideKeyboard
import jp.juggler.util.showToast
-import java.util.*
class ActText : AppCompatActivity(), View.OnClickListener {
@@ -32,281 +31,6 @@ class ActText : AppCompatActivity(), View.OnClickListener {
internal const val EXTRA_CONTENT_END = "content_end"
internal const val EXTRA_ACCOUNT_DB_ID = "account_db_id"
- private fun StringBuilder.addAfterLine( text : CharSequence) {
- if( isNotEmpty() && this[length - 1] != '\n') {
- append('\n')
- }
- append(text)
- }
-
- private fun addHeader(
- context : Context,
- sb : StringBuilder,
- key_str_id : Int,
- value : Any?
- ) {
- if(sb.isNotEmpty() && sb[sb.length - 1] != '\n') {
- sb.append('\n')
- }
- sb.addAfterLine( context.getString(key_str_id))
- sb.append(": ")
- sb.append(value?.toString() ?: "(null)")
- }
-
- private fun encodeStatus(
- intent : Intent,
- context : Context,
- access_info : SavedAccount,
- status : TootStatus
- ) {
- val sb = StringBuilder()
-
- addHeader(context, sb, R.string.send_header_url, status.url)
-
- addHeader(
- context,
- sb,
- R.string.send_header_date,
- TootStatus.formatTime(context, status.time_created_at, false)
- )
-
-
- addHeader(
- context,
- sb,
- R.string.send_header_from_acct,
- access_info.getFullAcct(status.account)
- )
-
- val sv : String? = status.spoiler_text
- if(sv != null && sv.isNotEmpty()) {
- addHeader(context, sb, R.string.send_header_content_warning, sv)
- }
-
- sb.addAfterLine( "\n")
-
- intent.putExtra(EXTRA_CONTENT_START, sb.length)
- sb.append(DecodeOptions(context, access_info).decodeHTML(status.content))
-
- encodePolls(sb,context,status)
-
- intent.putExtra(EXTRA_CONTENT_END, sb.length)
-
- dumpAttachment(sb, status.media_attachments)
-
- sb.addAfterLine( String.format(Locale.JAPAN, "Status-Source: %s", status.json))
-
- sb.addAfterLine( "")
- intent.putExtra(EXTRA_TEXT, sb.toString())
- }
-
-
-
- private fun dumpAttachment(sb : StringBuilder, src : ArrayList?) {
- if(src == null) return
- var i = 0
- for(ma in src) {
- ++ i
- if(ma is TootAttachment) {
- sb.addAfterLine( "\n")
- sb.addAfterLine( String.format(Locale.JAPAN, "Media-%d-Url: %s", i, ma.url))
- sb.addAfterLine(
- String.format(Locale.JAPAN, "Media-%d-Remote-Url: %s", i, ma.remote_url)
- )
- sb.addAfterLine(
- String.format(Locale.JAPAN, "Media-%d-Preview-Url: %s", i, ma.preview_url)
- )
- sb. addAfterLine(
- String.format(Locale.JAPAN, "Media-%d-Text-Url: %s", i, ma.text_url)
- )
- } else if(ma is TootAttachmentMSP) {
- sb.addAfterLine( "\n")
- sb. addAfterLine(
- String.format(Locale.JAPAN, "Media-%d-Preview-Url: %s", i, ma.preview_url)
- )
- }
- }
- }
-
-
- private fun encodePolls(sb :StringBuilder, context:Context,status : TootStatus) {
- val enquete = status.enquete ?: return
- val items = enquete.items ?: return
- val now = System.currentTimeMillis()
-
-
-
- val canVote = when(enquete.pollType) {
-
- // friends.nico の場合は本文に投票の選択肢が含まれるので
- // アプリ側での文字列化は不要
- TootPollsType.FriendsNico -> return
-
- // MastodonとMisskeyは投票の選択肢が本文に含まれないので
- // アプリ側で文字列化する
-
- TootPollsType.Mastodon -> when {
- enquete.expired -> false
- now >= enquete.expired_at -> false
- enquete.myVoted != null -> false
- else -> true
- }
-
- TootPollsType.Misskey -> enquete.myVoted == null
- }
-
- sb.addAfterLine("\n")
-
- items.forEachIndexed { index, choice ->
- encodePollChoice(sb, context, enquete, canVote, index, choice)
- }
-
- when(enquete.pollType) {
- TootPollsType.Mastodon -> encodePollFooterMastodon(sb, context, enquete)
-
- else->{}
- }
- }
-
- private fun encodePollChoice(
- sb : StringBuilder,
- context : Context,
- enquete : TootPolls,
- canVote : Boolean,
- i : Int,
- item : TootPollsChoice
- ) {
-
- val text = when(enquete.pollType) {
- TootPollsType.Misskey -> {
- val sb2 = StringBuilder().append(item.decoded_text)
- if(enquete.myVoted != null) {
- sb2.append(" / ")
- sb2.append(context.getString(R.string.vote_count_text, item.votes))
- if(i == enquete.myVoted) sb2.append(' ').append(0x2713.toChar())
- }
- sb2
- }
-
- TootPollsType.FriendsNico -> {
- item.decoded_text
- }
-
- TootPollsType.Mastodon -> if(canVote) {
- item.decoded_text
- } else {
- val sb2 = StringBuilder().append(item.decoded_text)
- if(! canVote) {
- sb2.append(" / ")
- sb2.append(
- when(val v = item.votes) {
- null -> context.getString(R.string.vote_count_unavailable)
- else -> context.getString(R.string.vote_count_text, v)
- }
- )
- }
- sb2
- }
- }
-
- sb.addAfterLine(text)
- }
-
- private fun encodePollFooterMastodon(
- sb : StringBuilder,
- context : Context,
- enquete : TootPolls
- ) {
- val line = StringBuilder()
-
- val votes_count = enquete.votes_count ?: 0
- when {
- votes_count == 1 -> line.append(context.getString(R.string.vote_1))
- votes_count > 1 -> line.append(context.getString(R.string.vote_2, votes_count))
- }
-
- when(val t = enquete.expired_at) {
-
- Long.MAX_VALUE -> {
- }
-
- else -> {
- if(line.isNotEmpty()) line.append(" ")
- line.append(
- context.getString(
- R.string.vote_expire_at,
- TootStatus.formatTime(context, t, false)
- )
- )
- }
- }
- sb.addAfterLine(line)
- }
-
- private fun encodeAccount(
- intent : Intent,
- context : Context,
- access_info : SavedAccount,
- who : TootAccount
- ) {
- val sb = StringBuilder()
-
- intent.putExtra(EXTRA_CONTENT_START, sb.length)
- sb.append(who.display_name)
- sb.append("\n")
- sb.append("@")
- sb.append(access_info.getFullAcct(who))
- sb.append("\n")
-
- intent.putExtra(EXTRA_CONTENT_START, sb.length)
- sb.append(who.url)
- intent.putExtra(EXTRA_CONTENT_END, sb.length)
-
- sb.addAfterLine( "\n")
-
- sb.append(DecodeOptions(context, access_info).decodeHTML(who.note))
-
- sb.addAfterLine( "\n")
-
- addHeader(context, sb, R.string.send_header_account_name, who.display_name)
- addHeader(context, sb, R.string.send_header_account_acct, access_info.getFullAcct(who))
- addHeader(context, sb, R.string.send_header_account_url, who.url)
-
- addHeader(context, sb, R.string.send_header_account_image_avatar, who.avatar)
- addHeader(
- context,
- sb,
- R.string.send_header_account_image_avatar_static,
- who.avatar_static
- )
- addHeader(context, sb, R.string.send_header_account_image_header, who.header)
- addHeader(
- context,
- sb,
- R.string.send_header_account_image_header_static,
- who.header_static
- )
-
- addHeader(context, sb, R.string.send_header_account_created_at, who.created_at)
- addHeader(context, sb, R.string.send_header_account_statuses_count, who.statuses_count)
- addHeader(
- context,
- sb,
- R.string.send_header_account_followers_count,
- who.followers_count
- )
- addHeader(
- context,
- sb,
- R.string.send_header_account_following_count,
- who.following_count
- )
- addHeader(context, sb, R.string.send_header_account_locked, who.locked)
-
- sb.addAfterLine("")
- intent.putExtra(EXTRA_TEXT, sb.toString())
- }
-
fun open(
activity : ActMain,
request_code : Int,
@@ -315,8 +39,7 @@ class ActText : AppCompatActivity(), View.OnClickListener {
) {
val intent = Intent(activity, ActText::class.java)
intent.putExtra(EXTRA_ACCOUNT_DB_ID, access_info.db_id)
- encodeStatus(intent, activity, access_info, status)
-
+ TootTextEncoder.encodeStatus(intent, activity, access_info, status)
activity.startActivityForResult(intent, request_code)
}
@@ -328,8 +51,7 @@ class ActText : AppCompatActivity(), View.OnClickListener {
) {
val intent = Intent(activity, ActText::class.java)
intent.putExtra(EXTRA_ACCOUNT_DB_ID, access_info.db_id)
- encodeAccount(intent, activity, access_info, who)
-
+ TootTextEncoder.encodeAccount(intent, activity, access_info, who)
activity.startActivityForResult(intent, request_code)
}
@@ -368,11 +90,11 @@ class ActText : AppCompatActivity(), View.OnClickListener {
etText.setText(sv)
// Android 9 以降ではフォーカスがないとsetSelectionできない
- if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
etText.requestFocus()
etText.hideKeyboard()
}
-
+
etText.setSelection(content_start, content_end)
}
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ChooseReceiver.kt b/app/src/main/java/jp/juggler/subwaytooter/ChooseReceiver.kt
new file mode 100644
index 00000000..717f2af6
--- /dev/null
+++ b/app/src/main/java/jp/juggler/subwaytooter/ChooseReceiver.kt
@@ -0,0 +1,24 @@
+package jp.juggler.subwaytooter
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import java.lang.ref.WeakReference
+
+class ChooseReceiver :BroadcastReceiver(){
+
+ companion object{
+ var lastComponentName: ComponentName? = null
+ var refCallback : WeakReference<()->Unit>? = null
+
+ fun setCallback(cb:()->Unit){
+ refCallback = WeakReference(cb)
+ }
+ }
+
+ override fun onReceive(context: Context,intent: Intent?) {
+ lastComponentName = intent?.extras?.get(Intent.EXTRA_CHOSEN_COMPONENT) as? ComponentName
+ refCallback?.get()?.invoke()
+ }
+}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/Pref.kt b/app/src/main/java/jp/juggler/subwaytooter/Pref.kt
index 76655f64..16c58753 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/Pref.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/Pref.kt
@@ -394,6 +394,13 @@ object Pref {
R.id.swCustomEmojiSeparatorZwsp
)
+ val bpShowTranslateButton = BooleanPref(
+ "ShowTranslateButton",
+ false,
+ R.id.swShowTranslateButton
+ )
+
+
// int
val ipBackButtonAction = IntPref("back_button_action", 0)
@@ -493,6 +500,8 @@ object Pref {
val spQuickTootMacro = StringPref("QuickTootMacro","")
val spQuickTootVisibility = StringPref("QuickTootVisibility","")
+ val spTranslateAppComponent = StringPref("TranslateAppComponent","")
+
// long
val lpTabletTootDefaultAccount = LongPref("tablet_toot_default_account", - 1L)
diff --git a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.kt b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.kt
index f72bac59..56bacb13 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.kt
@@ -1,13 +1,15 @@
package jp.juggler.subwaytooter
+import android.content.ComponentName
import android.content.Context
-import android.content.res.ColorStateList
-import androidx.core.content.ContextCompat
+import android.content.Intent
+import android.content.SharedPreferences
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.PopupWindow
+import androidx.core.content.ContextCompat
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
@@ -19,6 +21,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
+import jp.juggler.subwaytooter.util.TootTextEncoder
import jp.juggler.subwaytooter.util.startMargin
import jp.juggler.subwaytooter.view.CountImageButton
import jp.juggler.util.*
@@ -37,6 +40,16 @@ internal class StatusButtons(
companion object {
val log = LogCategory("StatusButtons")
+
+ fun String.toComponentName() : ComponentName? {
+ try {
+ val idx = indexOf('/')
+ if(idx >= 1) return ComponentName(substring(0 until idx), substring(idx + 1))
+ } catch(ex:Throwable) {
+ log.e(ex,"incorrect component name $this")
+ }
+ return null
+ }
}
private val access_info : SavedAccount
@@ -53,6 +66,7 @@ internal class StatusButtons(
private val llFollow2 = holder.llFollow2
private val btnFollow2 = holder.btnFollow2
private val ivFollowedBy2 = holder.ivFollowedBy2
+ private val btnTranslate = holder.btnTranslate
private val btnMore = holder.btnMore
private val color_normal = column.getContentColor()
@@ -69,6 +83,7 @@ internal class StatusButtons(
btnFavourite.setOnLongClickListener(this)
btnFollow2.setOnClickListener(this)
btnFollow2.setOnLongClickListener(this)
+ btnTranslate.setOnClickListener(this)
btnMore.setOnClickListener(this)
btnConversation.setOnClickListener(this)
btnConversation.setOnLongClickListener(this)
@@ -210,6 +225,15 @@ internal class StatusButtons(
relation
}
+ if(vg(btnTranslate, Pref.bpShowTranslateButton(activity.pref))) {
+ setButton(
+ btnTranslate,
+ true,
+ color_normal,
+ R.drawable.ic_translate,
+ activity.getString(R.string.translate)
+ )
+ }
}
private fun setButton(
@@ -234,6 +258,25 @@ internal class StatusButtons(
b.isEnabled = enabled
}
+ private fun setButton(
+ b : ImageButton,
+ enabled : Boolean,
+ color : Int,
+ drawableId : Int,
+ contentDescription : String
+ ) {
+ val alpha = Styler.boost_alpha
+ val d = createColoredDrawable(
+ activity,
+ drawableId,
+ color,
+ alpha
+ )
+ b.setImageDrawable(d)
+ b.contentDescription = contentDescription
+ b.isEnabled = enabled
+ }
+
override fun onClick(v : View) {
close_window?.dismiss()
@@ -381,6 +424,38 @@ internal class StatusButtons(
}
}
+ btnTranslate -> {
+
+ try {
+ val sv = TootTextEncoder.encodeStatusForTranslate(activity, access_info, status)
+
+ var cn = Pref.spTranslateAppComponent(activity.pref)
+ .toComponentName()
+ if(cn == null) {
+ cn = activity.getString(R.string.translate_app_component_default)
+ .toComponentName()
+ if(cn == null) {
+ showToast(
+ activity,
+ true,
+ "please check translate app component in app setting."
+ )
+ return
+ }
+ }
+
+ val intent = Intent()
+ intent.action = Intent.ACTION_SEND
+ intent.type = "text/plain"
+ intent.putExtra(Intent.EXTRA_TEXT, sv)
+ intent.component = cn
+ activity.startActivity(intent)
+ } catch(ex : Throwable) {
+ log.trace(ex)
+ showToast(activity, ex, "send failed.")
+ }
+ }
+
btnMore -> DlgContextMenu(
activity,
column,
@@ -463,6 +538,7 @@ class StatusButtonsViewHolder(
lateinit var llFollow2 : View
lateinit var btnFollow2 : ImageButton
lateinit var ivFollowedBy2 : ImageView
+ lateinit var btnTranslate : ImageButton
lateinit var btnMore : ImageButton
init {
@@ -555,6 +631,20 @@ class StatusButtonsViewHolder(
}.lparams(matchParent, matchParent)
}
+ btnTranslate = imageButton {
+ background = ContextCompat.getDrawable(
+ context,
+ R.drawable.btn_bg_transparent
+ )
+ setPadding(paddingH, paddingV, paddingH, paddingV)
+ scaleType = ImageView.ScaleType.FIT_CENTER
+
+ contentDescription = context.getString(R.string.translate)
+ imageResource = R.drawable.ic_translate
+ }.lparams(buttonHeight, buttonHeight) {
+ startMargin = marginBetween
+ }
+
btnMore = imageButton {
background = ContextCompat.getDrawable(
context,
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/TootTextEncoder.kt b/app/src/main/java/jp/juggler/subwaytooter/util/TootTextEncoder.kt
new file mode 100644
index 00000000..2e8f4ccb
--- /dev/null
+++ b/app/src/main/java/jp/juggler/subwaytooter/util/TootTextEncoder.kt
@@ -0,0 +1,307 @@
+package jp.juggler.subwaytooter.util
+
+import android.content.Context
+import android.content.Intent
+import jp.juggler.subwaytooter.ActText
+import jp.juggler.subwaytooter.R
+import jp.juggler.subwaytooter.api.entity.*
+import jp.juggler.subwaytooter.table.SavedAccount
+import java.util.*
+
+object TootTextEncoder {
+ private fun StringBuilder.addAfterLine( text : CharSequence) {
+ if( isNotEmpty() && this[length - 1] != '\n') {
+ append('\n')
+ }
+ append(text)
+ }
+
+ private fun addHeader(
+ context : Context,
+ sb : StringBuilder,
+ key_str_id : Int,
+ value : Any?
+ ) {
+ if(sb.isNotEmpty() && sb[sb.length - 1] != '\n') {
+ sb.append('\n')
+ }
+ sb.addAfterLine( context.getString(key_str_id))
+ sb.append(": ")
+ sb.append(value?.toString() ?: "(null)")
+ }
+
+ fun encodeStatus(
+ intent : Intent,
+ context : Context,
+ access_info : SavedAccount,
+ status : TootStatus
+ ) {
+ val sb = StringBuilder()
+
+ addHeader(context, sb, R.string.send_header_url, status.url)
+
+ addHeader(
+ context,
+ sb,
+ R.string.send_header_date,
+ TootStatus.formatTime(context, status.time_created_at, false)
+ )
+
+
+ addHeader(
+ context,
+ sb,
+ R.string.send_header_from_acct,
+ access_info.getFullAcct(status.account)
+ )
+
+ val sv : String? = status.spoiler_text
+ if(sv != null && sv.isNotEmpty()) {
+ addHeader(context, sb, R.string.send_header_content_warning, sv)
+ }
+
+ sb.addAfterLine( "\n")
+
+ intent.putExtra(ActText.EXTRA_CONTENT_START, sb.length)
+ sb.append(DecodeOptions(context, access_info).decodeHTML(status.content))
+
+ encodePolls(sb,context,status)
+
+ intent.putExtra(ActText.EXTRA_CONTENT_END, sb.length)
+
+ dumpAttachment(sb, status.media_attachments)
+
+ sb.addAfterLine( String.format(Locale.JAPAN, "Status-Source: %s", status.json))
+
+ sb.addAfterLine( "")
+ intent.putExtra(ActText.EXTRA_TEXT, sb.toString())
+ }
+
+
+ fun encodeStatusForTranslate(
+ context : Context,
+ access_info : SavedAccount,
+ status : TootStatus
+ ) :String {
+ val sb = StringBuilder()
+
+ val sv : String? = status.spoiler_text
+ if(sv != null && sv.isNotEmpty()) {
+ sb.append(sv).append("\n\n")
+ }
+
+ sb.append(DecodeOptions(context, access_info).decodeHTML(status.content))
+
+ encodePolls(sb,context,status)
+
+ return sb.toString()
+ }
+
+
+ private fun dumpAttachment(sb : StringBuilder, src : ArrayList?) {
+ if(src == null) return
+ var i = 0
+ for(ma in src) {
+ ++ i
+ if(ma is TootAttachment) {
+ sb.addAfterLine( "\n")
+ sb.addAfterLine( String.format(Locale.JAPAN, "Media-%d-Url: %s", i, ma.url))
+ sb.addAfterLine(
+ String.format(Locale.JAPAN, "Media-%d-Remote-Url: %s", i, ma.remote_url)
+ )
+ sb.addAfterLine(
+ String.format(Locale.JAPAN, "Media-%d-Preview-Url: %s", i, ma.preview_url)
+ )
+ sb. addAfterLine(
+ String.format(Locale.JAPAN, "Media-%d-Text-Url: %s", i, ma.text_url)
+ )
+ } else if(ma is TootAttachmentMSP) {
+ sb.addAfterLine( "\n")
+ sb. addAfterLine(
+ String.format(Locale.JAPAN, "Media-%d-Preview-Url: %s", i, ma.preview_url)
+ )
+ }
+ }
+ }
+
+
+ private fun encodePolls(sb :StringBuilder, context: Context, status : TootStatus) {
+ val enquete = status.enquete ?: return
+ val items = enquete.items ?: return
+ val now = System.currentTimeMillis()
+
+
+
+ val canVote = when(enquete.pollType) {
+
+ // friends.nico の場合は本文に投票の選択肢が含まれるので
+ // アプリ側での文字列化は不要
+ TootPollsType.FriendsNico -> return
+
+ // MastodonとMisskeyは投票の選択肢が本文に含まれないので
+ // アプリ側で文字列化する
+
+ TootPollsType.Mastodon -> when {
+ enquete.expired -> false
+ now >= enquete.expired_at -> false
+ enquete.myVoted != null -> false
+ else -> true
+ }
+
+ TootPollsType.Misskey -> enquete.myVoted == null
+ }
+
+ sb.addAfterLine("\n")
+
+ items.forEachIndexed { index, choice ->
+ encodePollChoice(sb, context, enquete, canVote, index, choice)
+ }
+
+ when(enquete.pollType) {
+ TootPollsType.Mastodon -> encodePollFooterMastodon(sb, context, enquete)
+
+ else->{}
+ }
+ }
+
+ private fun encodePollChoice(
+ sb : StringBuilder,
+ context : Context,
+ enquete : TootPolls,
+ canVote : Boolean,
+ i : Int,
+ item : TootPollsChoice
+ ) {
+
+ val text = when(enquete.pollType) {
+ TootPollsType.Misskey -> {
+ val sb2 = StringBuilder().append(item.decoded_text)
+ if(enquete.myVoted != null) {
+ sb2.append(" / ")
+ sb2.append(context.getString(R.string.vote_count_text, item.votes))
+ if(i == enquete.myVoted) sb2.append(' ').append(0x2713.toChar())
+ }
+ sb2
+ }
+
+ TootPollsType.FriendsNico -> {
+ item.decoded_text
+ }
+
+ TootPollsType.Mastodon -> if(canVote) {
+ item.decoded_text
+ } else {
+ val sb2 = StringBuilder().append(item.decoded_text)
+ if(! canVote) {
+ sb2.append(" / ")
+ sb2.append(
+ when(val v = item.votes) {
+ null -> context.getString(R.string.vote_count_unavailable)
+ else -> context.getString(R.string.vote_count_text, v)
+ }
+ )
+ }
+ sb2
+ }
+ }
+
+ sb.addAfterLine(text)
+ }
+
+ private fun encodePollFooterMastodon(
+ sb : StringBuilder,
+ context : Context,
+ enquete : TootPolls
+ ) {
+ val line = StringBuilder()
+
+ val votes_count = enquete.votes_count ?: 0
+ when {
+ votes_count == 1 -> line.append(context.getString(R.string.vote_1))
+ votes_count > 1 -> line.append(context.getString(R.string.vote_2, votes_count))
+ }
+
+ when(val t = enquete.expired_at) {
+
+ Long.MAX_VALUE -> {
+ }
+
+ else -> {
+ if(line.isNotEmpty()) line.append(" ")
+ line.append(
+ context.getString(
+ R.string.vote_expire_at,
+ TootStatus.formatTime(context, t, false)
+ )
+ )
+ }
+ }
+ sb.addAfterLine(line)
+ }
+
+ fun encodeAccount(
+ intent : Intent,
+ context : Context,
+ access_info : SavedAccount,
+ who : TootAccount
+ ) {
+ val sb = StringBuilder()
+
+ intent.putExtra(ActText.EXTRA_CONTENT_START, sb.length)
+ sb.append(who.display_name)
+ sb.append("\n")
+ sb.append("@")
+ sb.append(access_info.getFullAcct(who))
+ sb.append("\n")
+
+ intent.putExtra(ActText.EXTRA_CONTENT_START, sb.length)
+ sb.append(who.url)
+ intent.putExtra(ActText.EXTRA_CONTENT_END, sb.length)
+
+ sb.addAfterLine( "\n")
+
+ sb.append(DecodeOptions(context, access_info).decodeHTML(who.note))
+
+ sb.addAfterLine( "\n")
+
+ addHeader(context, sb, R.string.send_header_account_name, who.display_name)
+ addHeader(context, sb, R.string.send_header_account_acct, access_info.getFullAcct(who))
+ addHeader(context, sb, R.string.send_header_account_url, who.url)
+
+ addHeader(context, sb, R.string.send_header_account_image_avatar, who.avatar)
+ addHeader(
+ context,
+ sb,
+ R.string.send_header_account_image_avatar_static,
+ who.avatar_static
+ )
+ addHeader(context, sb, R.string.send_header_account_image_header, who.header)
+ addHeader(
+ context,
+ sb,
+ R.string.send_header_account_image_header_static,
+ who.header_static
+ )
+
+ addHeader(context, sb, R.string.send_header_account_created_at, who.created_at)
+ addHeader(context, sb, R.string.send_header_account_statuses_count, who.statuses_count)
+ addHeader(
+ context,
+ sb,
+ R.string.send_header_account_followers_count,
+ who.followers_count
+ )
+ addHeader(
+ context,
+ sb,
+ R.string.send_header_account_following_count,
+ who.following_count
+ )
+ addHeader(context, sb, R.string.send_header_account_locked, who.locked)
+
+ sb.addAfterLine("")
+ intent.putExtra(ActText.EXTRA_TEXT, sb.toString())
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_translate.xml b/app/src/main/res/drawable/ic_translate.xml
new file mode 100644
index 00000000..10841511
--- /dev/null
+++ b/app/src/main/res/drawable/ic_translate.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/act_app_setting_appearance.xml b/app/src/main/res/layout/act_app_setting_appearance.xml
index 79ebaf59..031adefa 100644
--- a/app/src/main/res/layout/act_app_setting_appearance.xml
+++ b/app/src/main/res/layout/act_app_setting_appearance.xml
@@ -647,6 +647,8 @@
+
+
diff --git a/app/src/main/res/layout/act_app_setting_behavior.xml b/app/src/main/res/layout/act_app_setting_behavior.xml
index 2df24ad8..cf758bd1 100644
--- a/app/src/main/res/layout/act_app_setting_behavior.xml
+++ b/app/src/main/res/layout/act_app_setting_behavior.xml
@@ -248,6 +248,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index fe996c73..9a677a8b 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -907,5 +907,10 @@
ハッシュタグ(特定ユーザから)
ユーザからの通知
%1$sからの通知
-
+ 翻訳ボタンを表示する
+ 翻訳
+ 翻訳アプリのComponentName
+ ACTION_SENDを受け取れるアプリがありません。
+ 翻訳アプリを選択した後に設定画面まで戻ってください
+ 翻訳アプリを選択する機能はAndroid5.1+で使えます。
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 97928df8..ba2bfac2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -900,5 +900,12 @@
Load some preferences from WebUI
Notifications from user
Notifications from %1$s
+ Show translate button
+ Translate
+ Translation app component
+ com.google.android.apps.translate/com.google.android.apps.translate.TranslateActivity
+ Missing app that can receive ACTION_SEND.
+ Select translation app, then back to app setting.
+ Sorry, translation app chooser works on Android 5.1+.
\ No newline at end of file