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 @@ + + + + + + + + + + + + + + + + + + + + +