とりあえずアプリサーバV2に対応する

This commit is contained in:
tateisu 2023-02-05 05:52:26 +09:00
parent ffdd474c73
commit 6c804a2a5d
213 changed files with 9383 additions and 5588 deletions

View File

@ -34,14 +34,14 @@ inline val AnkoContext<*>.resources: Resources
inline val AnkoContext<*>.assets: AssetManager inline val AnkoContext<*>.assets: AssetManager
get() = ctx.assets get() = ctx.assets
inline val AnkoContext<*>.defaultSharedPreferences: SharedPreferences //inline val AnkoContext<*>.defaultSharedPreferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(ctx) // get() = PreferenceManager.getDefaultSharedPreferences(ctx)
//
inline val Context.defaultSharedPreferences: SharedPreferences //inline val Context.defaultSharedPreferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(this) // get() = PreferenceManager.getDefaultSharedPreferences(this)
//
inline val Fragment.defaultSharedPreferences: SharedPreferences //inline val Fragment.defaultSharedPreferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(requireContext()) // get() = PreferenceManager.getDefaultSharedPreferences(requireContext())
inline val Fragment.act: Activity? inline val Fragment.act: Activity?
get() = activity get() = activity

View File

@ -1,14 +1,17 @@
import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.Detekt
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.regex.Matcher
import java.util.regex.Pattern
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' id("com.android.application")
apply plugin: 'kotlin-kapt' id("org.jetbrains.kotlin.android")
apply plugin: 'org.jetbrains.kotlin.plugin.serialization' id("org.jetbrains.kotlin.kapt")
apply plugin: 'com.google.gms.google-services' id("org.jetbrains.kotlin.plugin.serialization")
apply plugin: "io.gitlab.arturbosch.detekt" id("com.google.devtools.ksp").version("1.8.0-1.0.9")
id("io.gitlab.arturbosch.detekt")
}
android { android {
compileSdkVersion stCompileSdkVersion compileSdkVersion stCompileSdkVersion
@ -58,25 +61,30 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled false
shrinkResources true shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
lintOptions { lintOptions {
disable 'MissingTranslation' disable "MissingTranslation"
} }
} }
debug{ debug {
} }
} }
// Specifies comma-separated list of flavor dimensions. // Specifies comma-separated list of flavor dimensions.
flavorDimensions "rcOrDev" flavorDimensions "fcmType"
productFlavors { productFlavors {
rc { nofcm {
dimension "rcOrDev" dimension "fcmType"
versionNameSuffix "-noFcm"
}
fcm {
dimension "fcmType"
versionNameSuffix "-play"
} }
} }
@ -97,18 +105,18 @@ android {
packagingOptions { packagingOptions {
resources { resources {
excludes += ['/META-INF/{AL2.0,LGPL2.1}', 'META-INF/DEPENDENCIES'] excludes += ["/META-INF/{AL2.0,LGPL2.1}", "META-INF/DEPENDENCIES"]
// https://github.com/Kotlin/kotlinx.coroutines/issues/1064 // https://github.com/Kotlin/kotlinx.coroutines/issues/1064
pickFirsts += ['META-INF/atomicfu.kotlin_module'] pickFirsts += ["META-INF/atomicfu.kotlin_module"]
} }
} }
useLibrary 'android.test.base' useLibrary "android.test.base"
useLibrary 'android.test.mock' useLibrary "android.test.mock"
lint { lint {
warning 'DuplicatePlatformClasses' warning "DuplicatePlatformClasses"
} }
namespace 'jp.juggler.subwaytooter' namespace "jp.juggler.subwaytooter"
} }
@ -134,18 +142,20 @@ dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugarLibVersion" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugarLibVersion"
implementation(project(":base")) implementation(project(":base"))
implementation project(':colorpicker') implementation project(":colorpicker")
implementation project(':emoji') implementation project(":emoji")
implementation project(':apng_android') implementation project(":apng_android")
implementation project(':anko') implementation project(":anko")
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs') implementation fileTree(include: ["*.aar"], dir: "src/main/libs")
// implementation "org.conscrypt:conscrypt-android:$conscryptVersion" // implementation "org.conscrypt:conscrypt-android:$conscryptVersion"
api "org.conscrypt:conscrypt-android:$conscryptVersion" api "org.conscrypt:conscrypt-android:$conscryptVersion"
implementation "com.github.UnifiedPush:android-connector:2.1.1"
kapt "androidx.annotation:annotation:$androidxAnnotationVersion" kapt "androidx.annotation:annotation:$androidxAnnotationVersion"
kapt "androidx.room:room-compiler:$roomVersion"
kapt "com.github.bumptech.glide:compiler:$glideVersion" //kapt "com.github.bumptech.glide:compiler:$glideVersion"
ksp "com.github.bumptech.glide:ksp:$glideVersion"
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
@ -189,6 +199,29 @@ repositories {
mavenCentral() mavenCentral()
} }
def willApplyGoogleService() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Matcher matcher
matcher = Pattern.compile("assemble|generate", Pattern.CASE_INSENSITIVE).matcher(tskReqStr)
if (!matcher.find()) {
// not assemble or generate task.
return false
}
matcher = Pattern.compile("(?:assemble|generate)fcm\\w+", Pattern.CASE_INSENSITIVE).matcher(tskReqStr)
if (!matcher.find()) {
println "willApplyGoogleService=false. $tskReqStr"
return false
} else {
println "willApplyGoogleService=true. $tskReqStr"
return true
}
}
if (willApplyGoogleService()) apply plugin: "com.google.gms.google-services"
tasks.register("detektAll", Detekt) { tasks.register("detektAll", Detekt) {
description = "Custom DETEKT build for all modules" description = "Custom DETEKT build for all modules"
@ -247,3 +280,4 @@ tasks.register("detektAll", Detekt) {
sarif.outputLocation = file("$buildDir/reports/detekt/st-${name}.sarif") sarif.outputLocation = file("$buildDir/reports/detekt/st-${name}.sarif")
} }
} }

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application tools:ignore="MissingApplicationIcon">
<service
android:name="jp.juggler.subwaytooter.push.MyFcmService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -0,0 +1,16 @@
package jp.juggler.subwaytooter.push
import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.tasks.await
@Suppress("unused")
class FcmTokenLoader {
// com.google.firebase:firebase-messaging.20.3.0 以降
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_coroutines_version"
suspend fun getToken(): String? =
FirebaseMessaging.getInstance().token.await()
suspend fun deleteToken(){
FirebaseMessaging.getInstance().deleteToken().await()
}
}

View File

@ -0,0 +1,49 @@
package jp.juggler.subwaytooter.push
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import jp.juggler.util.log.LogCategory
import jp.juggler.util.os.checkAppForeground
import kotlinx.coroutines.runBlocking
/**
* FCMのイベントを受け取るサービス
* - IntentServiceの一種なのでワーカースレッドから呼ばれるrunBlockingして良し
*/
class MyFcmService : FirebaseMessagingService() {
companion object{
private val log = LogCategory("MyFcmService")
}
/**
* FCMデバイストークンが更新された
*/
override fun onNewToken(token: String) {
try {
checkAppForeground("MyFcmService.onNewToken")
fcmHandler.onTokenChanged(token)
} catch (ex: Throwable) {
log.e(ex, "onNewToken failed.")
} finally {
checkAppForeground("MyFcmService.onNewToken")
}
}
/**
* メッセージを受信した
* - ワーカースレッドから呼ばれるrunBlockingして良し
* - IntentServiceの一種なので呼び出しの間はネットワークを使えるなどある
*/
override fun onMessageReceived(remoteMessage: RemoteMessage) {
try {
checkAppForeground("MyFcmService.onMessageReceived")
runBlocking {
fcmHandler.onMessageReceived( remoteMessage.data)
}
} catch (ex: Throwable) {
log.e(ex, "onMessageReceived failed.")
} finally {
checkAppForeground("MyFcmService.onMessageReceived")
}
}
}

View File

@ -84,16 +84,17 @@
<application <application
android:name=".App1" android:name=".App1"
android:allowBackup="true" android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_spec" android:fullBackupContent="@xml/backup_spec"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:maxAspectRatio="100" android:maxAspectRatio="100"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme.Light" android:theme="@style/AppTheme.Light"
tools:ignore="DataExtractionRules,UnusedAttribute"> tools:ignore="DataExtractionRules,UnusedAttribute">
<!-- android:localeConfig="@xml/locales_config" -->
<activity <activity
android:name=".ActMain" android:name=".ActMain"
@ -362,23 +363,38 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<service
android:name=".MyFirebaseMessagingService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
android:exported="false" android:exported="false"
tools:node="merge"> tools:node="merge">
<meta-data <meta-data
android:name="jp.juggler.subwaytooter.global.GlobalInitializer" android:name="jp.juggler.subwaytooter.pref.LazyContextInitializer"
android:value="androidx.startup" />
<meta-data
android:name="jp.juggler.subwaytooter.push.FcmHandlerInitializer"
android:value="androidx.startup" />
<meta-data
android:name="jp.juggler.subwaytooter.pref.PrefDeviceInitializer"
android:value="androidx.startup" />
<meta-data
android:name="jp.juggler.subwaytooter.notification.NotificationChannelsInitializer"
android:value="androidx.startup" />
<meta-data
android:name="jp.juggler.subwaytooter.table.AppDatabaseHolderIniitalizer"
android:value="androidx.startup" /> android:value="androidx.startup" />
</provider> </provider>
<receiver
android:name=".push.UpMessageReceiver"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View File

@ -2,6 +2,7 @@ package jp.juggler.subwaytooter
import android.app.Activity import android.app.Activity
import android.content.ContentValues import android.content.ContentValues
import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
@ -15,24 +16,29 @@ import android.view.View
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.action.accountRemove
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.auth.AuthBase import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.notification.* import jp.juggler.subwaytooter.notification.*
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.push.PushBase
import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.*
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.AppDispatchers import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.coroutine.launchProgress import jp.juggler.util.coroutine.launchProgress
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption
import jp.juggler.util.media.ResizeConfig import jp.juggler.util.media.ResizeConfig
import jp.juggler.util.media.ResizeType import jp.juggler.util.media.ResizeType
import jp.juggler.util.media.createResizedBitmap import jp.juggler.util.media.createResizedBitmap
@ -124,6 +130,10 @@ class ActAccountSetting : AppCompatActivity(),
ActAccountSettingBinding.inflate(layoutInflater, null, false) ActAccountSettingBinding.inflate(layoutInflater, null, false)
} }
private val authRepo by lazy {
AuthRepo(this)
}
private lateinit var nameInvalidator: NetworkEmojiInvalidator private lateinit var nameInvalidator: NetworkEmojiInvalidator
private lateinit var noteInvalidator: NetworkEmojiInvalidator private lateinit var noteInvalidator: NetworkEmojiInvalidator
private lateinit var defaultTextInvalidator: NetworkEmojiInvalidator private lateinit var defaultTextInvalidator: NetworkEmojiInvalidator
@ -218,20 +228,22 @@ class ActAccountSetting : AppCompatActivity(),
initUI() initUI()
val a = intent.long(KEY_ACCOUNT_DB_ID) launchAndShowError {
?.let { SavedAccount.loadAccount(this, it) } val a = intent.long(KEY_ACCOUNT_DB_ID)
if (a == null) { ?.let { daoSavedAccount.loadAccount(it) }
finish() if (a == null) {
return finish()
return@launchAndShowError
}
supportActionBar?.subtitle = a.acct.pretty
loadUIFromData(a)
initializeProfile()
views.btnOpenBrowser.text =
getString(R.string.open_instance_website, account.apiHost.pretty)
} }
supportActionBar?.subtitle = a.acct.pretty
loadUIFromData(a)
initializeProfile()
views.btnOpenBrowser.text =
getString(R.string.open_instance_website, account.apiHost.pretty)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -336,7 +348,7 @@ class ActAccountSetting : AppCompatActivity(),
R.id.etFieldValue4 R.id.etFieldValue4
).map { findViewById(it) } ).map { findViewById(it) }
btnNotificationStyleEditReply.vg(PrefB.bpSeparateReplyNotificationGroup.invoke()) btnNotificationStyleEditReply.vg(PrefB.bpSeparateReplyNotificationGroup.value)
nameInvalidator = NetworkEmojiInvalidator(handler, etDisplayName) nameInvalidator = NetworkEmojiInvalidator(handler, etDisplayName)
noteInvalidator = NetworkEmojiInvalidator(handler, etNote) noteInvalidator = NetworkEmojiInvalidator(handler, etNote)
@ -504,72 +516,76 @@ class ActAccountSetting : AppCompatActivity(),
} }
private fun showAcctColor() { private fun showAcctColor() {
val sa = this.account val sa = this.account
val ac = AcctColor.load(sa) val ac = daoAcctColor.load(sa)
views.tvUserCustom.apply { views.tvUserCustom.apply {
backgroundColor = ac.color_bg backgroundColor = ac.colorBg
text = ac.nickname text = ac.nickname
textColor = ac.color_fg.notZero() ?: attrColor(R.attr.colorTimeSmall) textColor = ac.colorFg.notZero()
?: attrColor(R.attr.colorTimeSmall)
} }
} }
private fun saveUIToData() { private fun saveUIToData() {
if (!::account.isInitialized) return if (!::account.isInitialized) return
if (loadingBusy) return if (loadingBusy) return
account.visibility = visibility launchAndShowError {
views.apply { account.visibility = visibility
account.dont_hide_nsfw = swNSFWOpen.isChecked views.apply {
account.dont_show_timeout = swDontShowTimeout.isChecked account.dont_hide_nsfw = swNSFWOpen.isChecked
account.expand_cw = swExpandCW.isChecked account.dont_show_timeout = swDontShowTimeout.isChecked
account.default_sensitive = swMarkSensitive.isChecked account.expand_cw = swExpandCW.isChecked
account.notification_mention = cbNotificationMention.isChecked account.default_sensitive = swMarkSensitive.isChecked
account.notification_boost = cbNotificationBoost.isChecked account.notification_mention = cbNotificationMention.isChecked
account.notification_favourite = cbNotificationFavourite.isChecked account.notification_boost = cbNotificationBoost.isChecked
account.notification_follow = cbNotificationFollow.isChecked account.notification_favourite = cbNotificationFavourite.isChecked
account.notification_follow_request = cbNotificationFollowRequest.isChecked account.notification_follow = cbNotificationFollow.isChecked
account.notification_reaction = cbNotificationReaction.isChecked account.notification_follow_request = cbNotificationFollowRequest.isChecked
account.notification_vote = cbNotificationVote.isChecked account.notification_reaction = cbNotificationReaction.isChecked
account.notification_post = cbNotificationPost.isChecked account.notification_vote = cbNotificationVote.isChecked
account.notification_update = cbNotificationUpdate.isChecked account.notification_post = cbNotificationPost.isChecked
account.notification_status_reference = cbNotificationStatusReference.isChecked account.notification_update = cbNotificationUpdate.isChecked
account.notification_status_reference = cbNotificationStatusReference.isChecked
account.confirm_follow = cbConfirmFollow.isChecked account.confirm_follow = cbConfirmFollow.isChecked
account.confirm_follow_locked = cbConfirmFollowLockedUser.isChecked account.confirm_follow_locked = cbConfirmFollowLockedUser.isChecked
account.confirm_unfollow = cbConfirmUnfollow.isChecked account.confirm_unfollow = cbConfirmUnfollow.isChecked
account.confirm_boost = cbConfirmBoost.isChecked account.confirm_boost = cbConfirmBoost.isChecked
account.confirm_favourite = cbConfirmFavourite.isChecked account.confirm_favourite = cbConfirmFavourite.isChecked
account.confirm_unboost = cbConfirmUnboost.isChecked account.confirm_unboost = cbConfirmUnboost.isChecked
account.confirm_unfavourite = cbConfirmUnfavourite.isChecked account.confirm_unfavourite = cbConfirmUnfavourite.isChecked
account.confirm_post = cbConfirmToot.isChecked account.confirm_post = cbConfirmToot.isChecked
account.confirm_reaction = cbConfirmReaction.isChecked account.confirm_reaction = cbConfirmReaction.isChecked
account.confirm_unbookmark = cbConfirmUnbookmark.isChecked account.confirm_unbookmark = cbConfirmUnbookmark.isChecked
account.sound_uri = "" account.sound_uri = ""
account.default_text = etDefaultText.text.toString() account.default_text = etDefaultText.text.toString()
account.max_toot_chars = etMaxTootChars.parseInt()?.takeIf { it > 0 } ?: 0 account.max_toot_chars = etMaxTootChars.parseInt()?.takeIf { it > 0 } ?: 0
account.movie_max_megabytes = etMovieSizeMax.text.toString().trim() account.movie_max_megabytes = etMovieSizeMax.text.toString().trim()
account.image_max_megabytes = etMediaSizeMax.text.toString().trim() account.image_max_megabytes = etMediaSizeMax.text.toString().trim()
account.image_resize = ( account.image_resize = (
imageResizeItems.elementAtOrNull(spResizeImage.selectedItemPosition)?.config imageResizeItems.elementAtOrNull(spResizeImage.selectedItemPosition)?.config
?: SavedAccount.defaultResizeConfig ?: SavedAccount.defaultResizeConfig
).spec ).spec
account.push_policy = account.push_policy =
pushPolicyItems.elementAtOrNull(spPushPolicy.selectedItemPosition)?.id pushPolicyItems.elementAtOrNull(spPushPolicy.selectedItemPosition)?.id
account.movieTranscodeMode = spMovieTranscodeMode.selectedItemPosition account.movieTranscodeMode = spMovieTranscodeMode.selectedItemPosition
account.movieTranscodeBitrate = etMovieBitrate.text.toString() account.movieTranscodeBitrate = etMovieBitrate.text.toString()
account.movieTranscodeFramerate = etMovieFrameRate.text.toString() account.movieTranscodeFramerate = etMovieFrameRate.text.toString()
account.movieTranscodeSquarePixels = etMovieSquarePixels.text.toString() account.movieTranscodeSquarePixels = etMovieSquarePixels.text.toString()
account.lang = languages.elementAtOrNull(spLanguageCode.selectedItemPosition)?.first account.lang = languages.elementAtOrNull(spLanguageCode.selectedItemPosition)?.first
?: SavedAccount.LANG_WEB ?: SavedAccount.LANG_WEB
}
daoSavedAccount.saveSetting(account)
} }
account.saveSetting()
} }
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
@ -617,24 +633,20 @@ class ActAccountSetting : AppCompatActivity(),
R.id.btnFields -> sendFields() R.id.btnFields -> sendFields()
R.id.btnNotificationStyleEdit -> R.id.btnNotificationStyleEdit ->
MessageNotification.openNotificationChannelSetting( PullNotification.openNotificationChannelSetting(
this, this
account,
MessageNotification.TRACKING_NAME_DEFAULT
) )
R.id.btnNotificationStyleEditReply -> R.id.btnNotificationStyleEditReply ->
MessageNotification.openNotificationChannelSetting( PullNotification.openNotificationChannelSetting(
this, this
account,
MessageNotification.TRACKING_NAME_REPLY
) )
} }
} }
private fun showVisibility() { private fun showVisibility() {
views.btnVisibility.text = views.btnVisibility.text =
getVisibilityString(this, account.isMisskey, visibility) visibility.getVisibilityString(account.isMisskey)
} }
private fun performVisibility() { private fun performVisibility() {
@ -734,10 +746,11 @@ class ActAccountSetting : AppCompatActivity(),
.setMessage(R.string.confirm_account_remove) .setMessage(R.string.confirm_account_remove)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
accountRemove(account) launchAndShowError {
finish() authRepo.accountRemove(account)
} finish()
.show() }
}.show()
} }
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
@ -823,7 +836,7 @@ class ActAccountSetting : AppCompatActivity(),
result.jsonObject result.jsonObject
} else { } else {
// 承認待ち状態のチェック // 承認待ち状態のチェック
account.checkConfirmed(this, client) authRepo.checkConfirmed(account, client)
val result = client.request( val result = client.request(
"/api/v1/accounts/verify_credentials" "/api/v1/accounts/verify_credentials"
@ -1259,21 +1272,21 @@ class ActAccountSetting : AppCompatActivity(),
} }
private fun openPicker(permissionRequester: PermissionRequester) { private fun openPicker(permissionRequester: PermissionRequester) {
if (!permissionRequester.checkOrLaunch()) return launchAndShowError {
if (!permissionRequester.checkOrLaunch()) return@launchAndShowError
val propName = when (permissionRequester) { val propName = when (permissionRequester) {
prPickHeader -> "header" prPickHeader -> "header"
else -> "avatar" else -> "avatar"
}
actionsDialog {
action(getString(R.string.pick_image)) {
performAttachment(propName)
}
action(getString(R.string.image_capture)) {
performCamera(propName)
}
}
} }
val a = ActionsDialog()
a.addAction(getString(R.string.pick_image)) {
performAttachment(propName)
}
a.addAction(getString(R.string.image_capture)) {
performCamera(propName)
}
a.show(this, null)
} }
private fun performAttachment(propName: String) { private fun performAttachment(propName: String) {
@ -1416,19 +1429,57 @@ class ActAccountSetting : AppCompatActivity(),
} }
private fun updatePushSubscription(force: Boolean) { private fun updatePushSubscription(force: Boolean) {
val wps = PushSubscriptionHelper(applicationContext, account, verbose = true) val activity = this
launchMain { launchAndShowError {
runApiTask(account) { client -> val anyNotificationWanted = account.notification_boost ||
wps.updateSubscription(client, force = force) account.notification_favourite ||
}?.let { account.notification_follow ||
val log = wps.logString account.notification_mention ||
if (log.isNotEmpty()) { account.notification_reaction ||
AlertDialog.Builder(this@ActAccountSetting) account.notification_vote ||
.setMessage(log) account.notification_follow_request ||
.setPositiveButton(R.string.close, null) account.notification_post ||
.show() account.notification_update
val lines = ArrayList<String>()
val subLogger = object : PushBase.SubscriptionLogger {
override val context: Context
get() = activity
override fun i(msg: String) {
log.w(msg)
synchronized(lines) {
lines.add(msg)
}
}
override fun e(msg: String) {
log.e(msg)
synchronized(lines) {
lines.add(msg)
}
}
override fun e(ex: Throwable, msg: String) {
log.e(ex, msg)
synchronized(lines) {
lines.add(ex.withCaption(msg))
}
} }
} }
try {
pushRepo.updateSubscription(
subLogger,
account,
willRemoveSubscription = !anyNotificationWanted,
)
} catch (ex: Throwable) {
subLogger.e(ex, "updateSubscription failed.")
}
AlertDialog.Builder(activity)
.setMessage("${account.acct}:\n${lines.joinToString("\n")}")
.setPositiveButton(android.R.string.ok, null)
.show()
} }
} }
} }

View File

@ -0,0 +1,47 @@
package jp.juggler.subwaytooter
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import jp.juggler.subwaytooter.databinding.ActAlertBinding
import jp.juggler.util.data.encodePercent
import jp.juggler.util.data.notEmpty
import jp.juggler.util.ui.setNavigationBack
class ActAlert : AppCompatActivity() {
companion object {
private const val EXTRA_MESSAGE = "message"
private const val EXTRA_TITLE = "title"
fun Context.intentActAlert(
tag: String,
message: String,
title: String,
) = Intent(this, ActAlert::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
data = "app://error/${tag.encodePercent()}".toUri()
putExtra(EXTRA_MESSAGE, message)
putExtra(EXTRA_TITLE, title)
}
}
private val views by lazy {
ActAlertBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(views.root)
setSupportActionBar(views.toolbar)
setNavigationBack(views.toolbar)
intent?.getStringExtra(EXTRA_TITLE).notEmpty()
?.let { title = it }
intent?.getStringExtra(EXTRA_MESSAGE).notEmpty()
?.let { views.etMessage.setText(it) }
}
}

View File

@ -1,7 +1,6 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
@ -33,6 +32,7 @@ import jp.juggler.subwaytooter.appsetting.AppDataExporter
import jp.juggler.subwaytooter.appsetting.AppSettingItem import jp.juggler.subwaytooter.appsetting.AppSettingItem
import jp.juggler.subwaytooter.appsetting.SettingType import jp.juggler.subwaytooter.appsetting.SettingType
import jp.juggler.subwaytooter.appsetting.appSettingRoot import jp.juggler.subwaytooter.appsetting.appSettingRoot
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.databinding.ActAppSettingBinding import jp.juggler.subwaytooter.databinding.ActAppSettingBinding
import jp.juggler.subwaytooter.databinding.LvSettingItemBinding import jp.juggler.subwaytooter.databinding.LvSettingItemBinding
import jp.juggler.subwaytooter.dialog.DlgAppPicker import jp.juggler.subwaytooter.dialog.DlgAppPicker
@ -41,11 +41,9 @@ import jp.juggler.subwaytooter.pref.impl.BooleanPref
import jp.juggler.subwaytooter.pref.impl.FloatPref import jp.juggler.subwaytooter.pref.impl.FloatPref
import jp.juggler.subwaytooter.pref.impl.IntPref import jp.juggler.subwaytooter.pref.impl.IntPref
import jp.juggler.subwaytooter.pref.impl.StringPref import jp.juggler.subwaytooter.pref.impl.StringPref
import jp.juggler.subwaytooter.pref.pref import jp.juggler.subwaytooter.pref.lazyPref
import jp.juggler.subwaytooter.pref.put
import jp.juggler.subwaytooter.pref.remove
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.util.CustomShare import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.util.CustomShareTarget import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.cn import jp.juggler.subwaytooter.util.cn
@ -92,7 +90,6 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
private var customShareTarget: CustomShareTarget? = null private var customShareTarget: CustomShareTarget? = null
lateinit var pref: SharedPreferences
lateinit var handler: Handler lateinit var handler: Handler
val views by lazy { val views by lazy {
@ -103,6 +100,10 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
MyAdapter() MyAdapter()
} }
val authRepo by lazy {
AuthRepo(this)
}
private val arNoop = ActivityResultHandler(log) { } private val arNoop = ActivityResultHandler(log) { }
private val arImportAppData = ActivityResultHandler(log) { r -> private val arImportAppData = ActivityResultHandler(log) { r ->
@ -159,7 +160,6 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
App1.setActivityTheme(this) App1.setActivityTheme(this)
this.handler = App1.getAppState(this).handler this.handler = App1.getAppState(this).handler
this.pref = pref()
// val intent = this.intent // val intent = this.intent
// val layoutId = intent.getIntExtra(EXTRA_LAYOUT_ID, 0) // val layoutId = intent.getIntExtra(EXTRA_LAYOUT_ID, 0)
@ -218,12 +218,12 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
} }
private fun removeDefaultPref() { private fun removeDefaultPref() {
val e = pref.edit() val e = lazyPref.edit()
var changed = false var changed = false
appSettingRoot.scan { appSettingRoot.scan {
when { when {
(it.pref as? IntPref)?.noRemove == true -> Unit (it.pref as? IntPref)?.noRemove == true -> Unit
it.pref?.removeDefault(pref, e) == true -> changed = true it.pref?.removeDefault(lazyPref, e) == true -> changed = true
} }
} }
if (changed) e.apply() if (changed) e.apply()
@ -371,7 +371,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
SettingType.ColorAlpha -> newColor.notZero() ?: 1 SettingType.ColorAlpha -> newColor.notZero() ?: 1
else -> newColor or Color.BLACK else -> newColor or Color.BLACK
} }
pref.edit().put(ip, c).apply() ip.value = c
findItemViewHolder(colorTarget)?.showColor() findItemViewHolder(colorTarget)?.showColor()
colorTarget.changed(this) colorTarget.changed(this)
} }
@ -512,8 +512,6 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
private val tvDesc = views.tvDesc private val tvDesc = views.tvDesc
private val tvError = views.tvError private val tvError = views.tvError
private val pref = actAppSetting.pref
var item: AppSettingItem? = null var item: AppSettingItem? = null
private var bindingBusy = false private var bindingBusy = false
@ -575,7 +573,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
vg(false) // skip animation vg(false) // skip animation
text = name text = name
isEnabledAlpha = item.enabled isEnabledAlpha = item.enabled
isChecked = bp(pref) isChecked = bp.value
vg(true) vg(true)
} }
@ -586,7 +584,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
vg(false) // skip animation vg(false) // skip animation
actAppSetting.setSwitchColor(views.swSwitch) actAppSetting.setSwitchColor(views.swSwitch)
isEnabledAlpha = item.enabled isEnabledAlpha = item.enabled
isChecked = bp(pref) isChecked = bp.value
vg(true) vg(true)
} }
@ -608,12 +606,12 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
showCaption(name) showCaption(name)
views.llButtonBar.vg(true) views.llButtonBar.vg(true)
views.vColor.vg(true) views.vColor.vg(true)
views.vColor.setBackgroundColor(ip(pref)) views.vColor.setBackgroundColor(ip.value)
views.btnEdit.isEnabledAlpha = item.enabled views.btnEdit.isEnabledAlpha = item.enabled
views.btnReset.isEnabledAlpha = item.enabled views.btnReset.isEnabledAlpha = item.enabled
views.btnEdit.setOnClickListener { views.btnEdit.setOnClickListener {
actAppSetting.colorTarget = item actAppSetting.colorTarget = item
val color = ip(pref) val color = ip.value
val builder = ColorPickerDialog.newBuilder() val builder = ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM) .setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true) .setAllowPresets(true)
@ -623,7 +621,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
builder.show(actAppSetting) builder.show(actAppSetting)
} }
views.btnReset.setOnClickListener { views.btnReset.setOnClickListener {
pref.edit().remove(ip).apply() ip.removeValue()
showColor() showColor()
item.changed.invoke(actAppSetting) item.changed.invoke(actAppSetting)
} }
@ -644,7 +642,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
argsInt?.map { actAppSetting.getString(it) } argsInt?.map { actAppSetting.getString(it) }
?: item.spinnerArgsProc(actAppSetting) ?: item.spinnerArgsProc(actAppSetting)
) )
views.spSpinner.setSelection(pi.invoke(pref)) views.spSpinner.setSelection(pi.value)
} else { } else {
item.spinnerInitializer.invoke(actAppSetting, views.spSpinner) item.spinnerInitializer.invoke(actAppSetting, views.spSpinner)
} }
@ -655,9 +653,9 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
views.etEditText.vg(true)?.let { etEditText -> views.etEditText.vg(true)?.let { etEditText ->
val text = when (val pi = item.pref) { val text = when (val pi = item.pref) {
is FloatPref -> is FloatPref ->
item.fromFloat.invoke(actAppSetting, pi(pref)) item.fromFloat.invoke(actAppSetting, pi.value)
is StringPref -> is StringPref ->
pi(pref) pi.value
else -> error("EditText has incorrect pref $pi") else -> error("EditText has incorrect pref $pi")
} }
@ -736,7 +734,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
fun showColor() { fun showColor() {
val item = item ?: return val item = item ?: return
val ip = item.pref.cast<IntPref>() ?: return val ip = item.pref.cast<IntPref>() ?: return
val c = ip(pref) val c = ip.value
views.vColor.setBackgroundColor(c) views.vColor.setBackgroundColor(c)
} }
@ -753,15 +751,14 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
val sv = item.filter.invoke(p0?.toString() ?: "") val sv = item.filter.invoke(p0?.toString() ?: "")
when (val pi = item.pref) { when (val pi = item.pref) {
is StringPref -> is StringPref -> pi.value = sv
pref.edit().put(pi, sv).apply()
is FloatPref -> { is FloatPref -> {
val fv = item.toFloat.invoke(actAppSetting, sv) val fv = item.toFloat.invoke(actAppSetting, sv)
if (fv.isFinite()) { if (fv.isFinite()) {
pref.edit().put(pi, fv).apply() pi.value = fv
} else { } else {
pref.edit().remove(pi.key).apply() pi.removeValue()
} }
} }
@ -785,7 +782,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
if (bindingBusy) return if (bindingBusy) return
val item = item ?: return val item = item ?: return
when (val pi = item.pref) { when (val pi = item.pref) {
is IntPref -> pref.edit().put(pi, views.spSpinner.selectedItemPosition).apply() is IntPref -> pi.value = views.spSpinner.selectedItemPosition
else -> item.spinnerOnSelected.invoke(actAppSetting, views.spSpinner, position) else -> item.spinnerOnSelected.invoke(actAppSetting, views.spSpinner, position)
} }
item.changed.invoke(actAppSetting) item.changed.invoke(actAppSetting)
@ -795,7 +792,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
if (bindingBusy) return if (bindingBusy) return
val item = item ?: return val item = item ?: return
when (val pi = item.pref) { when (val pi = item.pref) {
is BooleanPref -> pref.edit().put(pi, isChecked).apply() is BooleanPref -> pi.value = isChecked
else -> error("CompoundButton has no booleanPref $pi") else -> error("CompoundButton has no booleanPref $pi")
} }
item.changed.invoke(actAppSetting) item.changed.invoke(actAppSetting)
@ -946,7 +943,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let { data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
val file = saveTimelineFont(it, fileName) val file = saveTimelineFont(it, fileName)
if (file != null) { if (file != null) {
pref.edit().put(item.pref.cast()!!, file.absolutePath).apply() (item.pref as? StringPref)?.value = file.absolutePath
showTimelineFont(item) showTimelineFont(item)
} }
} }
@ -959,19 +956,17 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
} }
fun showTimelineFont(item: AppSettingItem, tv: TextView) { fun showTimelineFont(item: AppSettingItem, tv: TextView) {
val fontUrl = item.pref.cast<StringPref>()!!.invoke(this)
try { try {
if (fontUrl.isNotEmpty()) { item.pref.cast<StringPref>()?.value.notEmpty()?.let { url ->
tv.typeface = Typeface.DEFAULT tv.typeface = Typeface.DEFAULT
val face = Typeface.createFromFile(fontUrl) val face = Typeface.createFromFile(url)
tv.typeface = face tv.typeface = face
tv.text = fontUrl tv.text = url
return return
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.e(ex, "showTimelineFont failed.") log.e(ex, "showTimelineFont failed.")
} }
// fallback // fallback
tv.text = getString(R.string.not_selected) tv.text = getString(R.string.not_selected)
tv.typeface = Typeface.DEFAULT tv.typeface = Typeface.DEFAULT
@ -1026,17 +1021,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
inner class AccountAdapter internal constructor() : BaseAdapter() { inner class AccountAdapter(val list: List<SavedAccount>) : BaseAdapter() {
internal val list = ArrayList<SavedAccount>()
init {
for (a in SavedAccount.loadAccountList(this@ActAppSetting)) {
if (a.isPseudo) continue
list.add(a)
}
SavedAccount.sort(list)
}
override fun getCount(): Int { override fun getCount(): Int {
return 1 + list.size return 1 + list.size
@ -1058,7 +1043,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
) )
view.findViewById<TextView>(android.R.id.text1).text = when (position) { view.findViewById<TextView>(android.R.id.text1).text = when (position) {
0 -> getString(R.string.ask_always) 0 -> getString(R.string.ask_always)
else -> AcctColor.getNickname(list[position - 1]) else -> daoAcctColor.getNickname(list[position - 1])
} }
return view return view
} }
@ -1068,7 +1053,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
viewOld ?: layoutInflater.inflate(R.layout.lv_spinner_dropdown, parent, false) viewOld ?: layoutInflater.inflate(R.layout.lv_spinner_dropdown, parent, false)
view.findViewById<TextView>(android.R.id.text1).text = when (position) { view.findViewById<TextView>(android.R.id.text1).text = when (position) {
0 -> getString(R.string.ask_always) 0 -> getString(R.string.ask_always)
else -> AcctColor.getNickname(list[position - 1]) else -> daoAcctColor.getNickname(list[position - 1])
} }
return view return view
} }
@ -1201,7 +1186,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
fun setCustomShare(appSettingItem: AppSettingItem, target: CustomShareTarget, value: String) { fun setCustomShare(appSettingItem: AppSettingItem, target: CustomShareTarget, value: String) {
val sp: StringPref = appSettingItem.pref.cast() ?: error("$target: not StringPref") val sp: StringPref = appSettingItem.pref.cast() ?: error("$target: not StringPref")
pref.edit().put(sp, value).apply() sp.value = value
showCustomShareIcon(findItemViewHolder(appSettingItem)?.views?.textView1, target) showCustomShareIcon(findItemViewHolder(appSettingItem)?.views?.textView1, target)
} }
@ -1238,7 +1223,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
private fun setWebBrowser(appSettingItem: AppSettingItem, value: String) { private fun setWebBrowser(appSettingItem: AppSettingItem, value: String) {
val sp: StringPref = appSettingItem.pref.cast() val sp: StringPref = appSettingItem.pref.cast()
?: error("${getString(appSettingItem.caption)}: not StringPref") ?: error("${getString(appSettingItem.caption)}: not StringPref")
pref.edit().put(sp, value).apply() sp.value = value
showWebBrowser(findItemViewHolder(appSettingItem)?.views?.textView1, value) showWebBrowser(findItemViewHolder(appSettingItem)?.views?.textView1, value)
} }

View File

@ -9,12 +9,14 @@ import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.databinding.ActWordListBinding import jp.juggler.subwaytooter.databinding.ActWordListBinding
import jp.juggler.subwaytooter.databinding.LvMuteAppBinding import jp.juggler.subwaytooter.databinding.LvMuteAppBinding
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.FavMute import jp.juggler.subwaytooter.table.daoFavMute
import jp.juggler.util.backPressed import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast import jp.juggler.util.data.cast
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.setNavigationBack import jp.juggler.util.ui.setNavigationBack
import kotlinx.coroutines.withContext
class ActFavMute : AppCompatActivity() { class ActFavMute : AppCompatActivity() {
@ -52,27 +54,20 @@ class ActFavMute : AppCompatActivity() {
item ?: return item ?: return
launchAndShowError { launchAndShowError {
confirm(R.string.delete_confirm, item.acct.pretty) confirm(R.string.delete_confirm, item.acct.pretty)
FavMute.delete(item.acct) daoFavMute.delete(item.acct)
listAdapter.remove(item) listAdapter.remove(item)
} }
} }
private fun loadData() { private fun loadData() {
listAdapter.items = buildList { launchAndShowError {
try { listAdapter.items = withContext(AppDispatchers.IO) {
FavMute.createCursor().use { cursor -> daoFavMute.listAll().map {
val idxId = cursor.getColumnIndex(FavMute.COL_ID) MyItem(
val idxName = cursor.getColumnIndex(FavMute.COL_ACCT) id = it.id,
while (cursor.moveToNext()) { acct = Acct.parse(it.acct),
val item = MyItem( )
id = cursor.getLong(idxId),
acct = Acct.parse(cursor.getString(idxName)),
)
add(item)
}
} }
} catch (ex: Throwable) {
log.e(ex, "loadData failed.")
} }
} }
} }

View File

@ -12,7 +12,9 @@ import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import jp.juggler.subwaytooter.databinding.ActHighlightEditBinding import jp.juggler.subwaytooter.databinding.ActHighlightEditBinding
import jp.juggler.subwaytooter.table.HighlightWord import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.subwaytooter.table.daoHighlightWord
import jp.juggler.util.backPressed import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.decodeJsonObject import jp.juggler.util.data.decodeJsonObject
import jp.juggler.util.data.mayUri import jp.juggler.util.data.mayUri
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
@ -83,32 +85,34 @@ class ActHighlightWordEdit
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
fun loadData(): HighlightWord? { launchAndShowError {
savedInstanceState?.getString(STATE_ITEM) fun loadData(): HighlightWord? {
?.decodeJsonObject() savedInstanceState?.getString(STATE_ITEM)
?.let { return HighlightWord(it) } ?.decodeJsonObject()
?.let { return HighlightWord(it) }
intent?.string(EXTRA_INITIAL_TEXT) intent?.string(EXTRA_INITIAL_TEXT)
?.let { return HighlightWord(it) } ?.let { return HighlightWord(it) }
intent?.long(EXTRA_ITEM_ID) intent?.long(EXTRA_ITEM_ID)
?.let { return HighlightWord.load(it) } ?.let { return daoHighlightWord.load(it) }
return null return null
}
val item = loadData()
if (item == null) {
log.d("missing source data")
finish()
return@launchAndShowError
}
this@ActHighlightWordEdit.item = item
views.etName.setText(item.name)
showSound()
showColor()
} }
val item = loadData()
if (item == null) {
log.d("missing source data")
finish()
return
}
this.item = item
views.etName.setText(item.name)
showSound()
showColor()
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -254,22 +258,26 @@ class ActHighlightWordEdit
} }
private fun save() { private fun save() {
uiToData() launchAndShowError {
uiToData()
val name = item.name
if (item.name.isEmpty()) { if (name.isNullOrBlank()) {
showToast(true, R.string.cant_leave_empty_keyword) showToast(true, R.string.cant_leave_empty_keyword)
return return@launchAndShowError
}
val other = daoHighlightWord.load(name)
if (other != null && other.id != item.id) {
showToast(true, R.string.cant_save_duplicated_keyword)
return@launchAndShowError
}
daoHighlightWord.save(applicationContext, item)
App1.getAppState(applicationContext).enableSpeech()
showToast(false, R.string.saved)
setResult(RESULT_OK)
finish()
} }
val other = HighlightWord.load(item.name)
if (other != null && other.id != item.id) {
showToast(true, R.string.cant_save_duplicated_keyword)
return
}
item.save(this)
showToast(false, R.string.saved)
setResult(RESULT_OK)
finish()
} }
} }

View File

@ -14,13 +14,16 @@ import jp.juggler.subwaytooter.databinding.ActHighlightListBinding
import jp.juggler.subwaytooter.databinding.LvHighlightWordBinding import jp.juggler.subwaytooter.databinding.LvHighlightWordBinding
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.HighlightWord import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.subwaytooter.table.daoHighlightWord
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast import jp.juggler.util.data.cast
import jp.juggler.util.data.mayUri import jp.juggler.util.data.mayUri
import jp.juggler.util.data.notBlank
import jp.juggler.util.data.notZero import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.errorEx
import jp.juggler.util.ui.* import jp.juggler.util.ui.*
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class ActHighlightWordList : AppCompatActivity() { class ActHighlightWordList : AppCompatActivity() {
@ -40,7 +43,7 @@ class ActHighlightWordList : AppCompatActivity() {
} }
} }
fun tryRingTone(context: Context, uri: Uri?): Boolean { private fun tryRingTone(context: Context, uri: Uri?): Boolean {
try { try {
uri?.let { RingtoneManager.getRingtone(context, it) } uri?.let { RingtoneManager.getRingtone(context, it) }
?.let { ringtone -> ?.let { ringtone ->
@ -129,21 +132,10 @@ class ActHighlightWordList : AppCompatActivity() {
} }
private fun loadData() { private fun loadData() {
try { launchAndShowError {
listAdapter.items = buildList { listAdapter.items = withContext(AppDispatchers.IO) {
HighlightWord.createCursor().use { cursor -> daoHighlightWord.listAll()
val colIdx = HighlightWord.ColIdx(cursor)
while (cursor.moveToNext()) {
try {
add(HighlightWord(cursor, colIdx))
} catch (ex: Throwable) {
log.e(ex, "load error.")
}
}
}
} }
} catch (ex: Throwable) {
errorEx(ex, "query error.")
} }
} }
@ -161,15 +153,17 @@ class ActHighlightWordList : AppCompatActivity() {
val activity = this val activity = this
launchAndShowError { launchAndShowError {
confirm(getString(R.string.delete_confirm, item.name)) confirm(getString(R.string.delete_confirm, item.name))
item.delete(activity) daoHighlightWord.delete(applicationContext, item)
listAdapter.remove(item) listAdapter.remove(item)
App1.getAppState(activity).enableSpeech()
} }
} }
private fun speech(item: HighlightWord?) { private fun speech(item: HighlightWord?) {
item ?: return item?.name?.notBlank()?.let {
App1.getAppState(this@ActHighlightWordList) App1.getAppState(this@ActHighlightWordList)
.addSpeech(item.name, dedupMode = DedupMode.None) .addSpeech(it, dedupMode = DedupMode.None)
}
} }
// リスト要素のViewHolder // リスト要素のViewHolder

View File

@ -12,12 +12,15 @@ import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.TootApiResult import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.databinding.ActKeywordFilterBinding import jp.juggler.subwaytooter.databinding.ActKeywordFilterBinding
import jp.juggler.subwaytooter.databinding.LvKeywordFilterBinding import jp.juggler.subwaytooter.databinding.LvKeywordFilterBinding
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.util.backPressed import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.int import jp.juggler.util.int
@ -96,59 +99,64 @@ class ActKeywordFilter : AppCompatActivity() {
private val deleteIds = HashSet<String>() private val deleteIds = HashSet<String>()
val authRepo by lazy {
AuthRepo(this)
}
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
backPressed { confirmBack() } backPressed { confirmBack() }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
App1.setActivityTheme(this) App1.setActivityTheme(this)
val intent = this.intent
// filter ID の有無はUIに影響するのでinitUIより先に初期化する
this.filterId = EntityId.from(intent, EXTRA_FILTER_ID)
val a = intent.long(EXTRA_ACCOUNT_DB_ID)
?.let { SavedAccount.loadAccount(this, it) }
if (a == null) {
finish()
return
}
this.account = a
initUI() initUI()
showAccount() launchAndShowError {
if (savedInstanceState == null) { // filter ID の有無はUIに影響するのでinitUIより先に初期化する
if (filterId != null) { filterId = EntityId.from(intent, EXTRA_FILTER_ID)
startLoading()
} else { val a = intent.long(EXTRA_ACCOUNT_DB_ID)
views.spExpire.setSelection(1) ?.let { daoSavedAccount.loadAccount(it) }
val initialText = intent.string(EXTRA_INITIAL_PHRASE)?.trim() ?: "" if (a == null) {
views.etTitle.setText(initialText) finish()
addKeywordArea(TootFilterKeyword(keyword = initialText)) return@launchAndShowError
} }
} else { account = a
savedInstanceState.getStringArrayList(STATE_DELETE_IDS)
?.let { deleteIds.addAll(it) }
savedInstanceState.getStringArrayList(STATE_KEYWORDS) showAccount()
?.mapNotNull { it?.decodeJsonObject() }
?.forEach { if (savedInstanceState == null) {
try { if (filterId != null) {
addKeywordArea(TootFilterKeyword(it)) startLoading()
} catch (ex: Throwable) { } else {
log.e(ex, "can't decode TootFilterKeyword") views.spExpire.setSelection(1)
} val initialText = intent.string(EXTRA_INITIAL_PHRASE)?.trim() ?: ""
views.etTitle.setText(initialText)
addKeywordArea(TootFilterKeyword(keyword = initialText))
} }
} else {
savedInstanceState.int(STATE_EXPIRE_SPINNER) savedInstanceState.getStringArrayList(STATE_DELETE_IDS)
?.let { views.spExpire.setSelection(it) } ?.let { deleteIds.addAll(it) }
savedInstanceState.long(STATE_EXPIRE_AT) savedInstanceState.getStringArrayList(STATE_KEYWORDS)
?.let { filterExpire = it } ?.mapNotNull { it?.decodeJsonObject() }
?.forEach {
try {
addKeywordArea(TootFilterKeyword(it))
} catch (ex: Throwable) {
log.e(ex, "can't decode TootFilterKeyword")
}
}
savedInstanceState.int(STATE_EXPIRE_SPINNER)
?.let { views.spExpire.setSelection(it) }
savedInstanceState.long(STATE_EXPIRE_AT)
?.let { filterExpire = it }
}
} }
} }
@ -218,7 +226,7 @@ class ActKeywordFilter : AppCompatActivity() {
} }
private fun showAccount() { private fun showAccount() {
views.tvAccount.text = AcctColor.getNicknameWithColor(account.acct) views.tvAccount.text = daoAcctColor.getNicknameWithColor(account.acct)
} }
private fun startLoading() { private fun startLoading() {

View File

@ -17,8 +17,9 @@ import androidx.core.content.FileProvider
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.column.Column import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.databinding.ActLanguageFilterBinding import jp.juggler.subwaytooter.databinding.ActLanguageFilterBinding
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchProgress import jp.juggler.util.coroutine.launchProgress
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -263,15 +264,17 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
R.id.btnAdd -> edit(null) R.id.btnAdd -> edit(null)
R.id.btnMore -> { R.id.btnMore -> {
ActionsDialog() launchAndShowError {
.addAction(getString(R.string.clear_all)) { actionsDialog {
languageList.clear() action(getString(R.string.clear_all)) {
languageList.add(MyItem(TootStatus.LANGUAGE_CODE_DEFAULT, true)) languageList.clear()
adapter.notifyDataSetChanged() languageList.add(MyItem(TootStatus.LANGUAGE_CODE_DEFAULT, true))
adapter.notifyDataSetChanged()
}
action(getString(R.string.export)) { export() }
action(getString(R.string.import_)) { import() }
} }
.addAction(getString(R.string.export)) { export() } }
.addAction(getString(R.string.import_)) { import() }
.show(this)
} }
} }
} }
@ -338,14 +341,18 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
val languageList = val languageList =
activity.languageNameMap.map { MyItem(it.key, true) }.sortedWith(languageComparator) activity.languageNameMap.map { MyItem(it.key, true) }.sortedWith(languageComparator)
btnPresets.setOnClickListener { btnPresets.setOnClickListener {
val ad = ActionsDialog() activity.run {
for (a in languageList) { launchAndShowError {
ad.addAction("${a.code} ${activity.getDesc(a)}") { actionsDialog(getString(R.string.presets)) {
etLanguage.setText(a.code) for (a in languageList) {
updateDesc() action("${a.code} ${activity.getDesc(a)}") {
etLanguage.setText(a.code)
updateDesc()
}
}
}
} }
} }
ad.show(activity, activity.getString(R.string.presets))
} }
etLanguage.setText(item?.code ?: "") etLanguage.setText(item?.code ?: "")

View File

@ -3,7 +3,6 @@ package jp.juggler.subwaytooter
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Build import android.os.Build
@ -19,6 +18,7 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import jp.juggler.subwaytooter.action.accessTokenPrompt import jp.juggler.subwaytooter.action.accessTokenPrompt
@ -32,17 +32,15 @@ import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.dialog.DlgQuickTootMenu import jp.juggler.subwaytooter.dialog.DlgQuickTootMenu
import jp.juggler.subwaytooter.itemviewholder.StatusButtonsPopup import jp.juggler.subwaytooter.itemviewholder.StatusButtonsPopup
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.*
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.pref.put
import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.span.MyClickableSpanHandler import jp.juggler.subwaytooter.span.MyClickableSpanHandler
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.MyDrawerLayout import jp.juggler.subwaytooter.view.MyDrawerLayout
import jp.juggler.subwaytooter.view.MyEditText import jp.juggler.subwaytooter.view.MyEditText
import jp.juggler.util.backPressed import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
import jp.juggler.util.int import jp.juggler.util.int
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -53,6 +51,9 @@ import jp.juggler.util.string
import jp.juggler.util.ui.ActivityResultHandler import jp.juggler.util.ui.ActivityResultHandler
import jp.juggler.util.ui.attrColor import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.isNotOk import jp.juggler.util.ui.isNotOk
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.internal.toHexString import okhttp3.internal.toHexString
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
@ -152,7 +153,6 @@ class ActMain : AppCompatActivity(),
lateinit var completionHelper: CompletionHelper lateinit var completionHelper: CompletionHelper
lateinit var pref: SharedPreferences
lateinit var handler: Handler lateinit var handler: Handler
lateinit var appState: AppState lateinit var appState: AppState
@ -196,7 +196,7 @@ class ActMain : AppCompatActivity(),
override fun run() { override fun run() {
handler.removeCallbacks(this) handler.removeCallbacks(this)
if (!isStartedEx) return if (!isStartedEx) return
if (PrefB.bpRelativeTimestamp(pref)) { if (PrefB.bpRelativeTimestamp.value) {
appState.columnList.forEach { it.fireRelativeTime() } appState.columnList.forEach { it.fireRelativeTime() }
handler.postDelayed(this, 10000L) handler.postDelayed(this, 10000L)
} }
@ -209,7 +209,7 @@ class ActMain : AppCompatActivity(),
set(value) { set(value) {
if (value != quickPostVisibility) { if (value != quickPostVisibility) {
quickPostVisibility = value quickPostVisibility = value
pref.edit().put(PrefS.spQuickTootVisibility, value.id.toString()).apply() PrefS.spQuickTootVisibility.value = value.id.toString()
showQuickPostVisibility() showQuickPostVisibility()
} }
} }
@ -262,7 +262,7 @@ class ActMain : AppCompatActivity(),
} }
val arAppSetting = ActivityResultHandler(log) { r -> val arAppSetting = ActivityResultHandler(log) { r ->
Column.reloadDefaultColor(this, pref) Column.reloadDefaultColor(this)
showFooterColor() showFooterColor()
updateColumnStrip() updateColumnStrip()
if (r.resultCode == RESULT_APP_DATA_IMPORT) { if (r.resultCode == RESULT_APP_DATA_IMPORT) {
@ -282,15 +282,17 @@ class ActMain : AppCompatActivity(),
} }
val arAccountSetting = ActivityResultHandler(log) { r -> val arAccountSetting = ActivityResultHandler(log) { r ->
updateColumnStrip() launchAndShowError {
appState.columnList.forEach { it.fireShowColumnHeader() } updateColumnStrip()
when (r.resultCode) { appState.columnList.forEach { it.fireShowColumnHeader() }
RESULT_OK -> r.data?.data?.let { openBrowser(it) } when (r.resultCode) {
RESULT_OK -> r.data?.data?.let { openBrowser(it) }
ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN -> ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN ->
r.data?.long(ActAccountSetting.EXTRA_DB_ID) r.data?.long(ActAccountSetting.EXTRA_DB_ID)
?.let { SavedAccount.loadAccount(this, it) } ?.let { daoSavedAccount.loadAccount(it) }
?.let { accessTokenPrompt(it.apiHost) } ?.let { accessTokenPrompt(it.apiHost) }
}
} }
} }
@ -323,10 +325,12 @@ class ActMain : AppCompatActivity(),
} }
} }
private val prNotification = permissionSpecNotification.requester { val prNotification = permissionSpecNotification.requester {
// 特に何もしない // 特に何もしない
} }
private var startAfterJob: WeakReference<Job>? = null
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// ライフサイクルイベント // ライフサイクルイベント
@ -352,11 +356,10 @@ class ActMain : AppCompatActivity(),
appState = App1.getAppState(this) appState = App1.getAppState(this)
handler = appState.handler handler = appState.handler
pref = appState.pref
density = appState.density density = appState.density
completionHelper = CompletionHelper(this, pref, appState.handler) completionHelper = CompletionHelper(this, appState.handler)
EmojiDecoder.useTwemoji = PrefB.bpUseTwemoji(pref) EmojiDecoder.useTwemoji = PrefB.bpUseTwemoji.value
acctPadLr = (0.5f + 4f * density).toInt() acctPadLr = (0.5f + 4f * density).toInt()
reloadTextSize() reloadTextSize()
@ -373,8 +376,6 @@ class ActMain : AppCompatActivity(),
if (savedInstanceState != null) { if (savedInstanceState != null) {
sharedIntent2?.let { handleSharedIntent(it) } sharedIntent2?.let { handleSharedIntent(it) }
} }
checkPrivacyPolicy()
} }
override fun onDestroy() { override fun onDestroy() {
@ -452,91 +453,94 @@ class ActMain : AppCompatActivity(),
sideMenuAdapter.onActivityStart() sideMenuAdapter.onActivityStart()
launchDialogs()
// 残りの処理はActivityResultの処理より後回しにしたい // 残りの処理はActivityResultの処理より後回しにしたい
handler.postDelayed(onStartAfter, 1L) lifecycleScope.launch {
prNotification.checkOrLaunch()
themeDefaultChangedDialog()
}
}
private val onStartAfter = Runnable {
benchmark("onStartAfter total") {
benchmark("sweepBuggieData") {
// バグいアカウントデータを消す
try { try {
SavedAccount.sweepBuggieData() delay(1L)
} catch (ex: Throwable) { benchmark("onStartAfter total") {
log.e(ex, "sweepBuggieData failed.")
}
}
val newAccounts = benchmark("loadAccountList") { benchmark("sweepBuggieData") {
SavedAccount.loadAccountList(this) // バグいアカウントデータを消す
} try {
daoSavedAccount.sweepBuggieData()
benchmark("removeColumnByAccount") { } catch (ex: Throwable) {
val setDbId = newAccounts.map { it.db_id }.toSet() log.e(ex, "sweepBuggieData failed.")
// アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない }
appState.columnList
.mapIndexedNotNull { index, column ->
when {
column.accessInfo.isNA -> index
setDbId.contains(column.accessInfo.db_id) -> index
else -> null
} }
}.takeIf { it.size != appState.columnCount }
?.let { setColumnsOrder(it) }
}
benchmark("fireColumnColor") { val newAccounts = benchmark("loadAccountList") {
// 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす daoSavedAccount.loadAccountList()
appState.columnList.forEach { column -> }
column.viewHolder?.lastAnnouncementShown = 0L
column.fireColumnColor() benchmark("removeColumnByAccount") {
val setDbId = newAccounts.map { it.db_id }.toSet()
// アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない
appState.columnList
.mapIndexedNotNull { index, column ->
when {
column.accessInfo.isNA -> index
setDbId.contains(column.accessInfo.db_id) -> index
else -> null
}
}.takeIf { it.size != appState.columnCount }
?.let { setColumnsOrder(it) }
}
benchmark("fireColumnColor") {
// 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす
appState.columnList.forEach { column ->
column.viewHolder?.lastAnnouncementShown = 0L
column.fireColumnColor()
}
}
benchmark("reloadAccountSetting") {
// 各カラムのアカウント設定を読み直す
reloadAccountSetting(newAccounts)
}
benchmark("refreshAfterPost") {
// 投稿直後ならカラムの再取得を行う
refreshAfterPost()
}
benchmark("column.onActivityStart") {
// 画面復帰時に再取得などを行う
appState.columnList.forEach { it.onActivityStart() }
}
benchmark("streamManager.onScreenStart") {
// 画面復帰時にストリーミング接続を開始する
appState.streamManager.onScreenStart()
}
benchmark("updateColumnStripSelection") {
// カラムの表示範囲インジケータを更新
updateColumnStripSelection(-1, -1f)
}
benchmark("fireShowContent") {
appState.columnList.forEach {
it.fireShowContent(reason = "ActMain onStart", reset = true)
}
}
benchmark("proc_updateRelativeTime") {
// 相対時刻表示の更新
procUpdateRelativeTime.run()
}
benchmark("enableSpeech") {
// スピーチの開始
appState.enableSpeech()
}
}
} catch (ex: Throwable) {
log.e(ex, "startAfter failed.")
} }
} }.let { startAfterJob = WeakReference(it) }
benchmark("reloadAccountSetting") {
// 各カラムのアカウント設定を読み直す
reloadAccountSetting(newAccounts)
}
benchmark("refreshAfterPost") {
// 投稿直後ならカラムの再取得を行う
refreshAfterPost()
}
benchmark("column.onActivityStart") {
// 画面復帰時に再取得などを行う
appState.columnList.forEach { it.onActivityStart() }
}
benchmark("streamManager.onScreenStart") {
// 画面復帰時にストリーミング接続を開始する
appState.streamManager.onScreenStart()
}
benchmark("updateColumnStripSelection") {
// カラムの表示範囲インジケータを更新
updateColumnStripSelection(-1, -1f)
}
benchmark("fireShowContent") {
appState.columnList.forEach {
it.fireShowContent(reason = "ActMain onStart", reset = true)
}
}
benchmark("proc_updateRelativeTime") {
// 相対時刻表示の更新
procUpdateRelativeTime.run()
}
benchmark("enableSpeech") {
// スピーチの開始
appState.enableSpeech()
}
} }
} }
override fun onStop() { override fun onStop() {
log.d("onStop") log.d("onStop")
isStartedEx = false isStartedEx = false
handler.removeCallbacks(onStartAfter) startAfterJob?.get()?.cancel()
startAfterJob = null
handler.removeCallbacks(procUpdateRelativeTime) handler.removeCallbacks(procUpdateRelativeTime)
completionHelper.closeAcctPopup() completionHelper.closeAcctPopup()
@ -583,7 +587,7 @@ class ActMain : AppCompatActivity(),
at android.os.Binder.execTransact (Binder.java:739) at android.os.Binder.execTransact (Binder.java:739)
*/ */
if (PrefB.bpDontScreenOff(pref)) { if (PrefB.bpDontScreenOff.value) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else { } else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@ -611,7 +615,7 @@ class ActMain : AppCompatActivity(),
{ env -> env.pager.currentItem }, { env -> env.pager.currentItem },
{ env -> env.visibleColumnsIndices.first }) { env -> env.visibleColumnsIndices.first })
log.d("ipLastColumnPos save $lastPos") log.d("ipLastColumnPos save $lastPos")
pref.edit().put(PrefI.ipLastColumnPos, lastPos).apply() PrefI.ipLastColumnPos.value = lastPos
appState.columnList.forEach { it.saveScrollPosition() } appState.columnList.forEach { it.saveScrollPosition() }
@ -701,10 +705,10 @@ class ActMain : AppCompatActivity(),
setContentView(R.layout.act_main) setContentView(R.layout.act_main)
quickPostVisibility = quickPostVisibility =
TootVisibility.parseSavedVisibility(PrefS.spQuickTootVisibility(pref)) TootVisibility.parseSavedVisibility(PrefS.spQuickTootVisibility.value)
?: quickPostVisibility ?: quickPostVisibility
Column.reloadDefaultColor(this, pref) Column.reloadDefaultColor(this)
galaxyBackgroundWorkaround() galaxyBackgroundWorkaround()

View File

@ -25,15 +25,14 @@ import com.google.android.exoplayer2.util.RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.databinding.ActMediaViewerBinding import jp.juggler.subwaytooter.databinding.ActMediaViewerBinding
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable
import jp.juggler.subwaytooter.global.appPref
import jp.juggler.subwaytooter.pref.PrefI import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.put
import jp.juggler.subwaytooter.util.permissionSpecMediaDownload import jp.juggler.subwaytooter.util.permissionSpecMediaDownload
import jp.juggler.subwaytooter.util.requester import jp.juggler.subwaytooter.util.requester
import jp.juggler.subwaytooter.view.PinchBitmapView import jp.juggler.subwaytooter.view.PinchBitmapView
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -304,7 +303,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
views.pbvImage.background = MediaBackgroundDrawable( views.pbvImage.background = MediaBackgroundDrawable(
context = views.root.context, context = views.root.context,
tileStep = tileStep, tileStep = tileStep,
kind = MediaBackgroundDrawable.Kind.fromIndex(PrefI.ipMediaBackground(this)) kind = MediaBackgroundDrawable.Kind.fromIndex(PrefI.ipMediaBackground.value)
) )
val enablePaging = mediaList.size > 1 val enablePaging = mediaList.size > 1
@ -444,7 +443,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
val url = when { val url = when {
forceLocalUrl -> ta.url forceLocalUrl -> ta.url
else -> ta.getLargeUrl(appPref) else -> ta.getLargeUrl()
} }
if (url == null) { if (url == null) {
showError("missing media attachment url.") showError("missing media attachment url.")
@ -573,7 +572,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
pbvImage.visible().setBitmap(null) pbvImage.visible().setBitmap(null)
} }
val urlList = ta.getLargeUrlList(appPref) val urlList = ta.getLargeUrlList()
if (urlList.isEmpty()) { if (urlList.isEmpty()) {
showError("missing media attachment url.") showError("missing media attachment url.")
return return
@ -650,7 +649,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
?: error("missing DownloadManager system service") ?: error("missing DownloadManager system service")
val url = if (ta is TootAttachment) { val url = if (ta is TootAttachment) {
ta.getLargeUrl(appPref) ta.getLargeUrl()
} else { } else {
null null
} ?: return } ?: return
@ -781,65 +780,80 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
} }
private fun more(ta: TootAttachmentLike) { private fun more(ta: TootAttachmentLike) {
val ad = ActionsDialog() launchAndShowError {
if (ta is TootAttachment) { actionsDialog {
val url = ta.getLargeUrl(appPref) ?: return fun addMoreMenu(
ad.addAction(getString(R.string.open_in_browser)) { share(Intent.ACTION_VIEW, url) } captionPrefix: String,
ad.addAction(getString(R.string.share_url)) { share(Intent.ACTION_SEND, url) } url: String?,
ad.addAction(getString(R.string.copy_url)) { copy(url) } @Suppress("SameParameterValue") action: String,
addMoreMenu(ad, "url", ta.url, Intent.ACTION_VIEW) ) {
addMoreMenu(ad, "remote_url", ta.remote_url, Intent.ACTION_VIEW) val uri = url.mayUri() ?: return
addMoreMenu(ad, "preview_url", ta.preview_url, Intent.ACTION_VIEW) val caption = getString(R.string.open_browser_of, captionPrefix)
addMoreMenu(ad, "preview_remote_url", ta.preview_remote_url, Intent.ACTION_VIEW) action(caption) {
addMoreMenu(ad, "text_url", ta.text_url, Intent.ACTION_VIEW) try {
} else if (ta is TootAttachmentMSP) { val intent = Intent(action, uri)
val url = ta.preview_url intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ad.addAction(getString(R.string.open_in_browser)) { share(Intent.ACTION_VIEW, url) } startActivity(intent)
ad.addAction(getString(R.string.share_url)) { share(Intent.ACTION_SEND, url) } } catch (ex: Throwable) {
ad.addAction(getString(R.string.copy_url)) { copy(url) } showToast(ex, "can't open app.")
} }
}
}
if (ta is TootAttachment) {
val url = ta.getLargeUrl()
if (url != null) {
action(getString(R.string.open_in_browser)) {
share(Intent.ACTION_VIEW, url)
}
action(getString(R.string.share_url)) {
share(Intent.ACTION_SEND, url)
}
action(getString(R.string.copy_url)) {
copy(url)
}
}
addMoreMenu("url", ta.url, Intent.ACTION_VIEW)
addMoreMenu("remote_url", ta.remote_url, Intent.ACTION_VIEW)
addMoreMenu("preview_url", ta.preview_url, Intent.ACTION_VIEW)
addMoreMenu("preview_remote_url", ta.preview_remote_url, Intent.ACTION_VIEW)
addMoreMenu("text_url", ta.text_url, Intent.ACTION_VIEW)
} else if (ta is TootAttachmentMSP) {
val url = ta.preview_url
action(getString(R.string.open_in_browser)) {
share(Intent.ACTION_VIEW, url)
}
action(getString(R.string.share_url)) {
share(Intent.ACTION_SEND, url)
}
action(getString(R.string.copy_url)) {
copy(url)
}
}
if (TootAttachmentType.Image == mediaList.elementAtOrNull(idx)?.type) { if (TootAttachmentType.Image == mediaList.elementAtOrNull(idx)?.type) {
ad.addAction(getString(R.string.background_pattern)) { mediaBackgroundDialog() } action(getString(R.string.background_pattern)) { mediaBackgroundDialog() }
} }
ad.show(this, null)
}
private fun addMoreMenu(
ad: ActionsDialog,
captionPrefix: String,
url: String?,
@Suppress("SameParameterValue") action: String,
) {
val uri = url.mayUri() ?: return
val caption = getString(R.string.open_browser_of, captionPrefix)
ad.addAction(caption) {
try {
val intent = Intent(action, uri)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
} catch (ex: Throwable) {
showToast(ex, "can't open app.")
} }
} }
} }
private fun mediaBackgroundDialog() { private fun mediaBackgroundDialog() {
val ad = ActionsDialog() launchAndShowError {
for (k in MediaBackgroundDrawable.Kind.values()) { actionsDialog(getString(R.string.background_pattern)) {
if (!k.isMediaBackground) continue for (k in MediaBackgroundDrawable.Kind.values()) {
ad.addAction(k.name) { if (!k.isMediaBackground) continue
val idx = k.toIndex() action(k.name) {
appPref.edit().put(PrefI.ipMediaBackground, idx).apply() val idx = k.toIndex()
views.pbvImage.background = MediaBackgroundDrawable( PrefI.ipMediaBackground.value = idx
context = views.root.context, views.pbvImage.background = MediaBackgroundDrawable(
tileStep = tileStep, context = views.root.context,
kind = k tileStep = tileStep,
) kind = k
)
}
}
} }
} }
ad.show(this, getString(R.string.background_pattern))
} }
/** /**

View File

@ -9,11 +9,14 @@ import jp.juggler.subwaytooter.databinding.ActWordListBinding
import jp.juggler.subwaytooter.databinding.LvMuteAppBinding import jp.juggler.subwaytooter.databinding.LvMuteAppBinding
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.MutedApp import jp.juggler.subwaytooter.table.MutedApp
import jp.juggler.subwaytooter.table.appDatabase
import jp.juggler.util.backPressed import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast import jp.juggler.util.data.cast
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.setNavigationBack import jp.juggler.util.ui.setNavigationBack
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ActMutedApp : AppCompatActivity() { class ActMutedApp : AppCompatActivity() {
@ -27,6 +30,8 @@ class ActMutedApp : AppCompatActivity() {
private val listAdapter by lazy { MyListAdapter() } private val listAdapter by lazy { MyListAdapter() }
private val daoMutedApp by lazy { MutedApp.Access(appDatabase) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
backPressed { backPressed {
@ -47,48 +52,33 @@ class ActMutedApp : AppCompatActivity() {
} }
private fun loadData() { private fun loadData() {
listAdapter.items = buildList { launchAndShowError {
try { listAdapter.items = withContext(Dispatchers.IO) {
MutedApp.createCursor().use { cursor -> daoMutedApp.listAll()
val idxId = cursor.getColumnIndex(MutedApp.COL_ID)
val idxName = cursor.getColumnIndex(MutedApp.COL_NAME)
while (cursor.moveToNext()) {
val item = MyItem(
id = cursor.getLong(idxId),
name = cursor.getString(idxName)
)
add(item)
}
}
} catch (ex: Throwable) {
log.e(ex, "loadData failed.")
} }
} }
} }
private fun delete(item: MyItem?) { private fun delete(item: MutedApp?) {
item ?: return item ?: return
launchAndShowError { launchAndShowError {
confirm(R.string.delete_confirm, item.name) confirm(R.string.delete_confirm, item.name)
MutedApp.delete(item.name) daoMutedApp.delete(item.name)
listAdapter.remove(item) listAdapter.remove(item)
} }
} }
// リスト要素のデータ
private class MyItem(val id: Long, val name: String)
// リスト要素のViewHolder // リスト要素のViewHolder
private inner class MyViewHolder(parent: ViewGroup?) { private inner class MyViewHolder(parent: ViewGroup?) {
val views = LvMuteAppBinding.inflate(layoutInflater, parent, false) val views = LvMuteAppBinding.inflate(layoutInflater, parent, false)
var lastItem: MyItem? = null var lastItem: MutedApp? = null
init { init {
views.root.tag = this views.root.tag = this
views.btnDelete.setOnClickListener { delete(lastItem) } views.btnDelete.setOnClickListener { delete(lastItem) }
} }
fun bind(item: MyItem?) { fun bind(item: MutedApp?) {
item ?: return item ?: return
lastItem = item lastItem = item
views.tvName.text = item.name views.tvName.text = item.name
@ -96,13 +86,13 @@ class ActMutedApp : AppCompatActivity() {
} }
private inner class MyListAdapter : BaseAdapter() { private inner class MyListAdapter : BaseAdapter() {
var items: List<MyItem> = emptyList() var items: List<MutedApp> = emptyList()
set(value) { set(value) {
field = value field = value
notifyDataSetChanged() notifyDataSetChanged()
} }
fun remove(item: MyItem) { fun remove(item: MutedApp) {
items = items.filter { it != item } items = items.filter { it != item }
} }

View File

@ -9,11 +9,14 @@ import jp.juggler.subwaytooter.databinding.ActWordListBinding
import jp.juggler.subwaytooter.databinding.LvMuteAppBinding import jp.juggler.subwaytooter.databinding.LvMuteAppBinding
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.UserRelation import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.util.backPressed import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast import jp.juggler.util.data.cast
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.setNavigationBack import jp.juggler.util.ui.setNavigationBack
import kotlinx.coroutines.withContext
class ActMutedPseudoAccount : AppCompatActivity() { class ActMutedPseudoAccount : AppCompatActivity() {
@ -47,62 +50,47 @@ class ActMutedPseudoAccount : AppCompatActivity() {
} }
private fun loadData() { private fun loadData() {
listAdapter.items = buildList { launchAndShowError {
try { listAdapter.items = withContext(AppDispatchers.IO) {
UserRelation.createCursorPseudoMuted().use { cursor -> daoUserRelation.listPseudoMuted()
val idxId = UserRelation.COL_ID.getIndex(cursor)
val idxName = UserRelation.COL_WHO_ID.getIndex(cursor)
while (cursor.moveToNext()) {
val item = MyItem(
id = cursor.getLong(idxId),
name = cursor.getString(idxName)
)
add(item)
}
}
} catch (ex: Throwable) {
log.e(ex, "loadData failed.")
} }
} }
} }
private fun delete(item: MyItem?) { private fun delete(item: UserRelation?) {
item ?: return item ?: return
launchAndShowError { launchAndShowError {
confirm(R.string.delete_confirm, item.name) confirm(R.string.delete_confirm, item.whoId)
UserRelation.deletePseudo(item.id) daoUserRelation.deletePseudo(item.id)
listAdapter.remove(item) listAdapter.remove(item)
} }
} }
// リスト要素のデータ
private class MyItem(val id: Long, val name: String)
// リスト要素のViewHolder // リスト要素のViewHolder
private inner class MyViewHolder(parent: ViewGroup?) { private inner class MyViewHolder(parent: ViewGroup?) {
val views = LvMuteAppBinding.inflate(layoutInflater, parent, false) val views = LvMuteAppBinding.inflate(layoutInflater, parent, false)
private var lastItem: MyItem? = null private var lastItem: UserRelation? = null
init { init {
views.root.tag = this views.root.tag = this
views.btnDelete.setOnClickListener { delete(lastItem) } views.btnDelete.setOnClickListener { delete(lastItem) }
} }
fun bind(item: MyItem?) { fun bind(item: UserRelation?) {
item ?: return item ?: return
lastItem = item lastItem = item
views.tvName.text = item.name views.tvName.text = item.whoId
} }
} }
private inner class MyListAdapter : BaseAdapter() { private inner class MyListAdapter : BaseAdapter() {
var items: List<MyItem> = emptyList() var items: List<UserRelation> = emptyList()
set(value) { set(value) {
field = value field = value
notifyDataSetChanged() notifyDataSetChanged()
} }
fun remove(item: MyItem) { fun remove(item: UserRelation) {
items = items.filter { it != item } items = items.filter { it != item }
} }

View File

@ -9,11 +9,14 @@ import jp.juggler.subwaytooter.databinding.ActWordListBinding
import jp.juggler.subwaytooter.databinding.LvMuteAppBinding import jp.juggler.subwaytooter.databinding.LvMuteAppBinding
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.MutedWord import jp.juggler.subwaytooter.table.MutedWord
import jp.juggler.subwaytooter.table.daoMutedWord
import jp.juggler.util.backPressed import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast import jp.juggler.util.data.cast
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.setNavigationBack import jp.juggler.util.ui.setNavigationBack
import kotlinx.coroutines.withContext
class ActMutedWord : AppCompatActivity() { class ActMutedWord : AppCompatActivity() {
@ -47,48 +50,33 @@ class ActMutedWord : AppCompatActivity() {
} }
private fun loadData() { private fun loadData() {
listAdapter.items = buildList { launchAndShowError {
try { listAdapter.items = withContext(AppDispatchers.IO) {
MutedWord.createCursor().use { cursor -> daoMutedWord.listAll()
val idxId = cursor.getColumnIndex(MutedWord.COL_ID)
val idxName = cursor.getColumnIndex(MutedWord.COL_NAME)
while (cursor.moveToNext()) {
val item = MyItem(
id = cursor.getLong(idxId),
name = cursor.getString(idxName)
)
add(item)
}
}
} catch (ex: Throwable) {
log.e(ex, "loadData failed.")
} }
} }
} }
private fun delete(item: MyItem?) { private fun delete(item: MutedWord?) {
item ?: return item ?: return
launchAndShowError { launchAndShowError {
confirm(R.string.delete_confirm, item.name) confirm(R.string.delete_confirm, item.name)
MutedWord.delete(item.name) daoMutedWord.delete(item.name)
listAdapter.remove(item) listAdapter.remove(item)
} }
} }
// リスト要素のデータ
private class MyItem(val id: Long, val name: String)
// リスト要素のViewHolder // リスト要素のViewHolder
private inner class MyViewHolder(parent: ViewGroup?) { private inner class MyViewHolder(parent: ViewGroup?) {
val views = LvMuteAppBinding.inflate(layoutInflater, parent, false) val views = LvMuteAppBinding.inflate(layoutInflater, parent, false)
private var lastItem: MyItem? = null private var lastItem: MutedWord? = null
init { init {
views.root.tag = this views.root.tag = this
views.btnDelete.setOnClickListener { delete(lastItem) } views.btnDelete.setOnClickListener { delete(lastItem) }
} }
fun bind(item: MyItem?) { fun bind(item: MutedWord?) {
item ?: return item ?: return
lastItem = item lastItem = item
views.tvName.text = item.name views.tvName.text = item.name
@ -96,13 +84,13 @@ class ActMutedWord : AppCompatActivity() {
} }
private inner class MyListAdapter : BaseAdapter() { private inner class MyListAdapter : BaseAdapter() {
var items: List<MyItem> = emptyList() var items: List<MutedWord> = emptyList()
set(value) { set(value) {
field = value field = value
notifyDataSetChanged() notifyDataSetChanged()
} }
fun remove(item: MyItem?) { fun remove(item: MutedWord?) {
items = items.filter { it != item } items = items.filter { it != item }
} }

View File

@ -14,8 +14,10 @@ import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import jp.juggler.subwaytooter.api.entity.Acct import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.databinding.ActNicknameBinding import jp.juggler.subwaytooter.databinding.ActNicknameBinding
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.util.backPressed import jp.juggler.util.backPressed
import jp.juggler.util.boolean import jp.juggler.util.boolean
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.mayUri import jp.juggler.util.data.mayUri
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.notZero import jp.juggler.util.data.notZero
@ -134,11 +136,11 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
views.tvAcct.text = acctPretty views.tvAcct.text = acctPretty
val ac = AcctColor.load(acctAscii, acctPretty) val ac = daoAcctColor.load(acctAscii)
colorBg = ac.color_bg colorBg = ac.colorBg
colorFg = ac.color_fg colorFg = ac.colorFg
views.etNickname.setText(ac.nickname) views.etNickname.setText(ac.nickname)
notificationSoundUri = ac.notification_sound notificationSoundUri = ac.notificationSound
loadingBusy = false loadingBusy = false
show() show()
@ -146,14 +148,17 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
private fun save() { private fun save() {
if (loadingBusy) return if (loadingBusy) return
AcctColor( launchAndShowError {
acctAscii, daoAcctColor.save(
acctPretty, System.currentTimeMillis(),
views.etNickname.text.toString().trim { it <= ' ' }, AcctColor(
colorFg, nicknameSave = views.etNickname.text.toString().trim { it <= ' ' },
colorBg, colorFg = colorFg,
notificationSoundUri colorBg = colorBg,
).save(System.currentTimeMillis()) notificationSoundSaved = notificationSoundUri ?: "",
)
)
}
} }
private fun show() { private fun show() {

View File

@ -2,7 +2,6 @@ package jp.juggler.subwaytooter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@ -29,11 +28,14 @@ import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.MyEditText import jp.juggler.subwaytooter.view.MyEditText
import jp.juggler.subwaytooter.view.MyNetworkImageView import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.* import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchIO import jp.juggler.util.coroutine.launchIO
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.GetContentResultEntry import jp.juggler.util.data.GetContentResultEntry
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.string
import jp.juggler.util.ui.ActivityResultHandler import jp.juggler.util.ui.ActivityResultHandler
import jp.juggler.util.ui.isNotOk import jp.juggler.util.ui.isNotOk
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -111,7 +113,6 @@ class ActPost : AppCompatActivity(),
lateinit var etChoices: List<MyEditText> lateinit var etChoices: List<MyEditText>
lateinit var handler: Handler lateinit var handler: Handler
lateinit var pref: SharedPreferences
lateinit var appState: AppState lateinit var appState: AppState
lateinit var attachmentUploader: AttachmentUploader lateinit var attachmentUploader: AttachmentUploader
lateinit var attachmentPicker: AttachmentPicker lateinit var attachmentPicker: AttachmentPicker
@ -138,7 +139,7 @@ class ActPost : AppCompatActivity(),
var states = ActPostStates() var states = ActPostStates()
var accountList: ArrayList<SavedAccount> = ArrayList() var accountList: List<SavedAccount> = emptyList()
var account: SavedAccount? = null var account: SavedAccount? = null
var attachmentList = ArrayList<PostAttachment>() var attachmentList = ArrayList<PostAttachment>()
var isPostComplete: Boolean = false var isPostComplete: Boolean = false
@ -169,16 +170,17 @@ class ActPost : AppCompatActivity(),
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
backPressed { backPressed {
finish() launchAndShowError {
// 戻るボタンを押したときとonPauseで2回保存することになるが、 finish()
// 同じ内容はDB上は重複しないはず… // 戻るボタンを押したときとonPauseで2回保存することになるが、
saveDraft() // 同じ内容はDB上は重複しないはず…
saveDraft()
}
} }
if (isMultiWindowPost) ActMain.refActMain?.get()?.closeList?.add(WeakReference(this)) if (isMultiWindowPost) ActMain.refActMain?.get()?.closeList?.add(WeakReference(this))
App1.setActivityTheme(this) App1.setActivityTheme(this)
appState = App1.getAppState(this) appState = App1.getAppState(this)
handler = appState.handler handler = appState.handler
pref = appState.pref
attachmentUploader = AttachmentUploader(this, handler) attachmentUploader = AttachmentUploader(this, handler)
attachmentPicker = AttachmentPicker(this, this) attachmentPicker = AttachmentPicker(this, this)
density = resources.displayMetrics.density density = resources.displayMetrics.density
@ -202,9 +204,11 @@ class ActPost : AppCompatActivity(),
initUI() initUI()
when (savedInstanceState) { launchAndShowError {
null -> updateText(intent, confirmed = true, saveDraft = false) when (savedInstanceState) {
else -> restoreState(savedInstanceState) null -> updateText(intent, saveDraft = false)
else -> restoreState(savedInstanceState)
}
} }
} }
@ -242,11 +246,18 @@ class ActPost : AppCompatActivity(),
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
// 編集中にホーム画面を押したり他アプリに移動する場合は下書きを保存する if (!isPostComplete) launchMain {
// やや過剰な気がするが、自アプリに戻ってくるときにランチャーからアイコンタップされると try {
// メイン画面より上にあるアクティビティはすべて消されてしまうので // 編集中にホーム画面を押したり他アプリに移動する場合は下書きを保存する
// このタイミングで保存するしかない // やや過剰な気がするが、自アプリに戻ってくるときにランチャーからアイコンタップされると
if (!isPostComplete) saveDraft() // メイン画面より上にあるアクティビティはすべて消されてしまうので
// このタイミングで保存するしかない
saveDraft()
} catch (ex: Throwable) {
log.e(ex, "can't save draft.")
showToast(ex, "can't save draft.")
}
}
} }
override fun onClick(v: View) { override fun onClick(v: View) {
@ -317,7 +328,7 @@ class ActPost : AppCompatActivity(),
fun initUI() { fun initUI() {
setContentView(views.root) setContentView(views.root)
if (PrefB.bpPostButtonBarTop(pref)) { if (PrefB.bpPostButtonBarTop.value) {
val bar = findViewById<View>(R.id.llFooterBar) val bar = findViewById<View>(R.id.llFooterBar)
val parent = bar.parent as ViewGroup val parent = bar.parent as ViewGroup
parent.removeView(bar) parent.removeView(bar)
@ -401,7 +412,7 @@ class ActPost : AppCompatActivity(),
views.cbContentWarning.setOnCheckedChangeListener { _, _ -> showContentWarningEnabled() } views.cbContentWarning.setOnCheckedChangeListener { _, _ -> showContentWarningEnabled() }
completionHelper = CompletionHelper(this, pref, appState.handler) completionHelper = CompletionHelper(this, appState.handler)
completionHelper.attachEditText( completionHelper.attachEditText(
views.root, views.root,
views.etContent, views.etContent,

View File

@ -9,15 +9,18 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.api.entity.TootAccount import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.databinding.ActTextBinding import jp.juggler.subwaytooter.databinding.ActTextBinding
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.MutedWord
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoMutedWord
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.CustomShare import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.util.CustomShareTarget import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.TootTextEncoder import jp.juggler.subwaytooter.util.TootTextEncoder
import jp.juggler.subwaytooter.util.copyToClipboard import jp.juggler.subwaytooter.util.copyToClipboard
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -114,28 +117,34 @@ class ActText : AppCompatActivity() {
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
private val authRepo by lazy {
AuthRepo(this)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
App1.setActivityTheme(this) App1.setActivityTheme(this)
account = intent.long(EXTRA_ACCOUNT_DB_ID)
?.let { SavedAccount.loadAccount(this, it) }
initUI() initUI()
if (savedInstanceState == null) { launchAndShowError {
val sv = intent.string(EXTRA_TEXT) ?: "" account = intent.long(EXTRA_ACCOUNT_DB_ID)
val contentStart = intent.int(EXTRA_CONTENT_START) ?: 0 ?.let { daoSavedAccount.loadAccount(it) }
val contentEnd = intent.int(EXTRA_CONTENT_END) ?: sv.length
views.etText.setText(sv)
// Android 9 以降ではフォーカスがないとsetSelectionできない if (savedInstanceState == null) {
if (Build.VERSION.SDK_INT >= 28) { val sv = intent.string(EXTRA_TEXT) ?: ""
views.etText.requestFocus() val contentStart = intent.int(EXTRA_CONTENT_START) ?: 0
views.etText.hideKeyboard() val contentEnd = intent.int(EXTRA_CONTENT_END) ?: sv.length
views.etText.setText(sv)
// Android 9 以降ではフォーカスがないとsetSelectionできない
if (Build.VERSION.SDK_INT >= 28) {
views.etText.requestFocus()
views.etText.hideKeyboard()
}
views.etText.setSelection(contentStart, contentEnd)
} }
views.etText.setSelection(contentStart, contentEnd)
} }
} }
@ -192,14 +201,11 @@ class ActText : AppCompatActivity() {
} }
private fun muteWord() { private fun muteWord() {
selection.trim().notEmpty()?.let { launchAndShowError {
try { selection.trim().notEmpty()?.let {
MutedWord.save(it) daoMutedWord.save(it)
App1.getAppState(this).onMuteUpdated() App1.getAppState(this@ActText).onMuteUpdated()
showToast(false, R.string.word_was_muted) showToast(false, R.string.word_was_muted)
} catch (ex: Throwable) {
log.e(ex, "muteWord failed.")
showToast(ex, "muteWord failed.")
} }
} }
} }

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.util.Log import android.util.Log
@ -19,8 +20,7 @@ import com.bumptech.glide.load.model.GlideUrl
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.emoji.EmojiMap import jp.juggler.subwaytooter.emoji.EmojiMap
import jp.juggler.subwaytooter.global.Global import jp.juggler.subwaytooter.pref.LazyContextHolder
import jp.juggler.subwaytooter.global.appPref
import jp.juggler.subwaytooter.pref.PrefI import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.PrefS import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.table.HighlightWord import jp.juggler.subwaytooter.table.HighlightWord
@ -30,10 +30,12 @@ import jp.juggler.subwaytooter.util.CustomEmojiLister
import jp.juggler.subwaytooter.util.ProgressResponseBody import jp.juggler.subwaytooter.util.ProgressResponseBody
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.data.asciiPattern import jp.juggler.util.data.asciiPattern
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.initializeToastUtils import jp.juggler.util.log.initializeToastUtils
import jp.juggler.util.network.MySslSocketFactory import jp.juggler.util.network.MySslSocketFactory
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.os.applicationContextSafe
import jp.juggler.util.ui.* import jp.juggler.util.ui.*
import okhttp3.* import okhttp3.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -55,11 +57,17 @@ class App1 : Application() {
override fun onCreate() { override fun onCreate() {
log.d("onCreate") log.d("onCreate")
LazyContextHolder.init(applicationContextSafe)
super.onCreate() super.onCreate()
initializeToastUtils(this) initializeToastUtils(this)
prepare(applicationContext, "App1.onCreate") prepare(applicationContext, "App1.onCreate")
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
LazyContextHolder.init(applicationContextSafe)
}
override fun onTerminate() { override fun onTerminate() {
log.d("onTerminate") log.d("onTerminate")
super.onTerminate() super.onTerminate()
@ -134,7 +142,7 @@ class App1 : Application() {
"SubwayTooter/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE}" "SubwayTooter/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE}"
private fun getUserAgent(): String { private fun getUserAgent(): String {
val userAgentCustom = PrefS.spUserAgent(appPref) val userAgentCustom = PrefS.spUserAgent.value
return when { return when {
userAgentCustom.isNotEmpty() && !reNotAllowedInUserAgent.matcher(userAgentCustom) userAgentCustom.isNotEmpty() && !reNotAllowedInUserAgent.matcher(userAgentCustom)
.find() -> userAgentCustom .find() -> userAgentCustom
@ -142,13 +150,14 @@ class App1 : Application() {
} }
} }
private val user_agent_interceptor = Interceptor { chain -> private fun userAgentInterceptor() =
chain.proceed( Interceptor { chain ->
chain.request().newBuilder() chain.proceed(
.header("User-Agent", getUserAgent()) chain.request().newBuilder()
.build() .header("User-Agent", getUserAgent())
) .build()
} )
}
private var cookieManager: CookieManager? = null private var cookieManager: CookieManager? = null
private var cookieJar: CookieJar? = null private var cookieJar: CookieJar? = null
@ -185,7 +194,7 @@ class App1 : Application() {
.connectionSpecs(Collections.singletonList(spec)) .connectionSpecs(Collections.singletonList(spec))
.sslSocketFactory(MySslSocketFactory, MySslSocketFactory.trustManager) .sslSocketFactory(MySslSocketFactory, MySslSocketFactory.trustManager)
.addInterceptor(ProgressResponseBody.makeInterceptor()) .addInterceptor(ProgressResponseBody.makeInterceptor())
.addInterceptor(user_agent_interceptor) .addInterceptor(userAgentInterceptor())
// クッキーの導入は検討中。とりあえずmstdn.jpではクッキー有効でも改善しなかったので現時点では追加しない // クッキーの導入は検討中。とりあえずmstdn.jpではクッキー有効でも改善しなかったので現時点では追加しない
// .cookieJar(cookieJar) // .cookieJar(cookieJar)
@ -228,8 +237,6 @@ class App1 : Application() {
initializeFont() initializeFont()
Global.prepare(appContext, "App1.prepare($caller)")
// We want at least 2 threads and at most 4 threads in the core pool, // We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating // preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work // the CPU with background work
@ -278,7 +285,7 @@ class App1 : Application() {
Logger.getLogger(OkHttpClient::class.java.name).level = Level.FINE Logger.getLogger(OkHttpClient::class.java.name).level = Level.FINE
val apiReadTimeout = max(3, PrefS.spApiReadTimeout.toInt(appPref)) val apiReadTimeout = max(3, PrefS.spApiReadTimeout.toInt())
// API用のHTTP設定はキャッシュを使わない // API用のHTTP設定はキャッシュを使わない
ok_http_client = prepareOkHttp(apiReadTimeout, apiReadTimeout) ok_http_client = prepareOkHttp(apiReadTimeout, apiReadTimeout)
@ -294,10 +301,11 @@ class App1 : Application() {
.build() .build()
// 内蔵メディアビューア用のHTTP設定はタイムアウトを調整可能 // 内蔵メディアビューア用のHTTP設定はタイムアウトを調整可能
val mediaReadTimeout = max(3, PrefS.spMediaReadTimeout.toInt(appPref)) val mediaReadTimeout = max(3, PrefS.spMediaReadTimeout.toInt())
ok_http_client_media_viewer = prepareOkHttp(mediaReadTimeout, mediaReadTimeout) ok_http_client_media_viewer =
.cache(cache) prepareOkHttp(mediaReadTimeout, mediaReadTimeout)
.build() .cache(cache)
.build()
} }
val handler = Handler(appContext.mainLooper) val handler = Handler(appContext.mainLooper)
@ -310,7 +318,7 @@ class App1 : Application() {
log.d("create AppState.") log.d("create AppState.")
state = AppState(appContext, handler, appPref) state = AppState(appContext, handler)
appStateX = state appStateX = state
// getAppState()を使える状態にしてからカラム一覧をロードする // getAppState()を使える状態にしてからカラム一覧をロードする
@ -421,7 +429,7 @@ class App1 : Application() {
) { ) {
prepare(activity.applicationContext, "setActivityTheme") prepare(activity.applicationContext, "setActivityTheme")
var nTheme = PrefI.ipUiTheme(appPref) var nTheme = PrefI.ipUiTheme.value
if (forceDark && nTheme == 0) nTheme = 1 if (forceDark && nTheme == 0) nTheme = 1
activity.setTheme( activity.setTheme(
when (nTheme) { when (nTheme) {
@ -484,9 +492,8 @@ class App1 : Application() {
.url(url) .url(url)
.cacheControl(CACHE_CONTROL) .cacheControl(CACHE_CONTROL)
.also { .also {
val access_token = accessInfo?.getAccessToken() accessInfo?.bearerAccessToken?.notEmpty()?.let { a ->
if (access_token?.isNotEmpty() == true) { it.header("Authorization", "Bearer $a")
it.header("Authorization", "Bearer $access_token")
} }
} }
} }

View File

@ -17,10 +17,7 @@ import jp.juggler.subwaytooter.column.getBackgroundImageDir
import jp.juggler.subwaytooter.column.onMuteUpdated import jp.juggler.subwaytooter.column.onMuteUpdated
import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.streaming.StreamManager import jp.juggler.subwaytooter.streaming.StreamManager
import jp.juggler.subwaytooter.table.HighlightWord import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.table.MutedApp
import jp.juggler.subwaytooter.table.MutedWord
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.NetworkStateTracker import jp.juggler.subwaytooter.util.NetworkStateTracker
import jp.juggler.subwaytooter.util.PostAttachment import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.util.* import jp.juggler.util.*
@ -49,7 +46,6 @@ class DedupItem(
class AppState( class AppState(
internal val context: Context, internal val context: Context,
internal val handler: Handler, internal val handler: Handler,
internal val pref: SharedPreferences,
) { ) {
companion object { companion object {
@ -190,7 +186,7 @@ class AppState(
// TextToSpeech // TextToSpeech
private val isTextToSpeechRequired: Boolean private val isTextToSpeechRequired: Boolean
get() = columnList.any { it.enableSpeech } || HighlightWord.hasTextToSpeechHighlightWord() get() = columnList.any { it.enableSpeech } || daoHighlightWord.hasTextToSpeechHighlightWord()
private val ttsReceiver = object : BroadcastReceiver() { private val ttsReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
@ -303,8 +299,8 @@ class AppState(
if (list != null) editColumnList(save = false) { it.addAll(list) } if (list != null) editColumnList(save = false) { it.addAll(list) }
// ミュートデータのロード // ミュートデータのロード
TootStatus.muted_app = MutedApp.nameSet TootStatus.muted_app = daoMutedApp.nameSet()
TootStatus.muted_word = MutedWord.nameSet TootStatus.muted_word = daoMutedWord.nameSet()
// 背景フォルダの掃除 // 背景フォルダの掃除
try { try {
@ -599,8 +595,8 @@ class AppState(
} }
fun onMuteUpdated() { fun onMuteUpdated() {
TootStatus.muted_app = MutedApp.nameSet TootStatus.muted_app = daoMutedApp.nameSet()
TootStatus.muted_word = MutedWord.nameSet TootStatus.muted_word = daoMutedWord.nameSet()
columnList.forEach { it.onMuteUpdated() } columnList.forEach { it.onMuteUpdated() }
} }
} }

View File

@ -5,10 +5,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import jp.juggler.subwaytooter.notification.TrackingType import jp.juggler.subwaytooter.notification.TrackingType
import jp.juggler.subwaytooter.notification.onNotificationDeleted import jp.juggler.subwaytooter.notification.onNotificationDeleted
import jp.juggler.subwaytooter.table.NotificationTracking import jp.juggler.subwaytooter.table.daoNotificationTracking
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.os.applicationContextSafe
class EventReceiver : BroadcastReceiver() { class EventReceiver : BroadcastReceiver() {
@ -18,32 +19,36 @@ class EventReceiver : BroadcastReceiver() {
} }
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
launchMain {
try {
log.i("onReceive action=${intent?.action}")
log.i("onReceive action=${intent?.action}") when (val action = intent?.action) {
when (val action = intent?.action) { Intent.ACTION_BOOT_COMPLETED,
Intent.ACTION_MY_PACKAGE_REPLACED,
Intent.ACTION_BOOT_COMPLETED, -> {
Intent.ACTION_MY_PACKAGE_REPLACED, App1.prepare(context.applicationContextSafe, action)
-> { daoNotificationTracking.resetPostAll()
App1.prepare(context.applicationContext, action)
NotificationTracking.resetPostAll()
}
ACTION_NOTIFICATION_DELETE -> intent.data?.let { uri ->
val dbId = uri.getQueryParameter("db_id")?.toLongOrNull()
val type = TrackingType.parseStr(uri.getQueryParameter("type"))
val typeName = type.typeName
val id = uri.getQueryParameter("notificationId")?.notEmpty()
log.d("Notification deleted! db_id=$dbId,type=$type,id=$id")
if (dbId != null) {
launchMain {
onNotificationDeleted(dbId, typeName)
} }
}
}
else -> log.e("onReceive: unsupported action $action") ACTION_NOTIFICATION_DELETE,
-> intent.data?.let { uri ->
val dbId = uri.getQueryParameter("db_id")?.toLongOrNull()
val type = TrackingType.parseStr(uri.getQueryParameter("type"))
val typeName = type.typeName
val id = uri.getQueryParameter("notificationId")?.notEmpty()
log.d("Notification deleted! db_id=$dbId,type=$type,id=$id")
if (dbId != null) {
onNotificationDeleted(dbId, typeName)
}
}
else -> log.e("onReceive: unsupported action $action")
}
} catch (ex: Throwable) {
log.e(ex, "resetPostAll failed.")
}
} }
} }
} }

View File

@ -1,99 +1,101 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
//
import com.google.firebase.messaging.FirebaseMessagingService //import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage //import com.google.firebase.messaging.RemoteMessage
import jp.juggler.subwaytooter.notification.PollingChecker //import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.notification.restartAllWorker //import jp.juggler.subwaytooter.notification.PollingChecker
import jp.juggler.subwaytooter.pref.PrefDevice //import jp.juggler.subwaytooter.notification.restartAllWorker
import jp.juggler.subwaytooter.table.NotificationCache //import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.table.SavedAccount //import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.util.log.LogCategory //import jp.juggler.subwaytooter.table.SavedAccount
import kotlinx.coroutines.runBlocking //import jp.juggler.subwaytooter.table.apiNotificationCache
import java.util.* //import jp.juggler.subwaytooter.table.apiSavedAccount
//import jp.juggler.util.log.LogCategory
class MyFirebaseMessagingService : FirebaseMessagingService() { //import kotlinx.coroutines.runBlocking
//import java.util.*
companion object { //
internal val log = LogCategory("MyFirebaseMessagingService") //class MyFirebaseMessagingService : FirebaseMessagingService() {
//
private val pushMessageStatus = LinkedList<String>() // companion object {
// internal val log = LogCategory("MyFirebaseMessagingService")
// Pushメッセージが処理済みか調べる //
private fun isDuplicateMessage(messageId: String) = // private val pushMessageStatus = LinkedList<String>()
synchronized(pushMessageStatus) { //
when (pushMessageStatus.contains(messageId)) { // // Pushメッセージが処理済みか調べる
true -> true // private fun isDuplicateMessage(messageId: String) =
else -> { // synchronized(pushMessageStatus) {
pushMessageStatus.addFirst(messageId) // when (pushMessageStatus.contains(messageId)) {
while (pushMessageStatus.size > 100) { // true -> true
pushMessageStatus.removeLast() // else -> {
} // pushMessageStatus.addFirst(messageId)
false // while (pushMessageStatus.size > 100) {
} // pushMessageStatus.removeLast()
} // }
} // false
} // }
// }
override fun onNewToken(token: String) { // }
try { // }
log.w("onTokenRefresh: token=$token") //
PrefDevice.from(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply() // override fun onNewToken(token: String) {
restartAllWorker(this) // try {
} catch (ex: Throwable) { // log.w("onTokenRefresh: token=$token")
log.e(ex, "onNewToken failed") // prefDevice.device
} // pollingWorker2IntervalPrefDevice.from(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply()
} // restartAllWorker(this)
// } catch (ex: Throwable) {
override fun onMessageReceived(remoteMessage: RemoteMessage) { // log.e(ex, "onNewToken failed")
val context = this // }
// }
val messageId = remoteMessage.messageId ?: return //
if (isDuplicateMessage(messageId)) return // override fun onMessageReceived(remoteMessage: RemoteMessage) {
// val messageId = remoteMessage.messageId ?: return
val accounts = ArrayList<SavedAccount>() // if (isDuplicateMessage(messageId)) return
for ((key, value) in remoteMessage.data) { //
log.w("onMessageReceived: $key=$value") // val accounts = ArrayList<SavedAccount>()
when (key) { // for ((key, value) in remoteMessage.data) {
"notification_tag" -> { // log.w("onMessageReceived: $key=$value")
SavedAccount.loadByTag(context, value).forEach { sa -> // when (key) {
NotificationCache.resetLastLoad(sa.db_id) //// "notification_tag" -> {
accounts.add(sa) //// apiSavedAccount.(context, value).forEach { sa ->
} //// apiNotificationCache.resetLastLoad(sa.db_id)
} //// accounts.add(sa)
"acct" -> { //// }
SavedAccount.loadAccountByAcct(context, value)?.let { sa -> //// }
NotificationCache.resetLastLoad(sa.db_id) // "acct" -> {
accounts.add(sa) // apiSavedAccount.loadAccountByAcct(Acct.parse(value))?.let { sa ->
} // apiNotificationCache.resetLastLoad(sa.db_id)
} // accounts.add(sa)
} // }
} // }
// }
if (accounts.isEmpty()) { // }
// タグにマッチする情報がなかった場合、全部読み直す //
NotificationCache.resetLastLoad() // if (accounts.isEmpty()) {
accounts.addAll(SavedAccount.loadAccountList(context)) // // タグにマッチする情報がなかった場合、全部読み直す
} // apiNotificationCache.resetLastLoad()
// accounts.addAll(apiSavedAccount.loadAccountList())
log.i("accounts.size=${accounts.size} thred=${Thread.currentThread().name}") // }
runBlocking { //
accounts.forEach { // log.i("accounts.size=${accounts.size} thred=${Thread.currentThread().name}")
check(it.db_id) // runBlocking {
} // accounts.forEach {
} // check(it.db_id)
} // }
// }
private suspend fun check(accountDbId: Long) { // }
try { //
PollingChecker( // private suspend fun check(accountDbId: Long) {
context = this, // try {
accountDbId = accountDbId // PollingChecker(
).check { a, s -> // context = this,
val text = "[${a.acct.pretty}]${s.desc}" // accountDbId = accountDbId
log.i(text) // ).check { a, s ->
} // val text = "[${a.acct.pretty}]${s.desc}"
} catch (ex: Throwable) { // log.i(text)
log.e(ex, "check failed. accountDbId=$accountDbId") // }
} // } catch (ex: Throwable) {
} // log.e(ex, "check failed. accountDbId=$accountDbId")
} // }
// }
//}

View File

@ -23,7 +23,7 @@ import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.emoji.EmojiMap import jp.juggler.subwaytooter.emoji.EmojiMap
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.pref import jp.juggler.subwaytooter.pref.lazyContext
import jp.juggler.subwaytooter.span.EmojiImageSpan import jp.juggler.subwaytooter.span.EmojiImageSpan
import jp.juggler.subwaytooter.span.createSpan import jp.juggler.subwaytooter.span.createSpan
import jp.juggler.subwaytooter.table.UserRelation import jp.juggler.subwaytooter.table.UserRelation
@ -43,14 +43,14 @@ fun defaultColorIcon(context: Context, iconId: Int): Drawable? =
it.setTintMode(PorterDuff.Mode.SRC_IN) it.setTintMode(PorterDuff.Mode.SRC_IN)
} }
fun getVisibilityIconId(isMisskeyData: Boolean, visibility: TootVisibility): Int { fun TootVisibility.getVisibilityIconId(isMisskeyData: Boolean): Int {
val isMisskey = when (PrefI.ipVisibilityStyle()) { val isMisskey = when (PrefI.ipVisibilityStyle.value) {
PrefI.VS_MASTODON -> false PrefI.VS_MASTODON -> false
PrefI.VS_MISSKEY -> true PrefI.VS_MISSKEY -> true
else -> isMisskeyData else -> isMisskeyData
} }
return when { return when {
isMisskey -> when (visibility) { isMisskey -> when (this) {
TootVisibility.Public -> R.drawable.ic_public TootVisibility.Public -> R.drawable.ic_public
TootVisibility.UnlistedHome -> R.drawable.ic_home TootVisibility.UnlistedHome -> R.drawable.ic_home
TootVisibility.PrivateFollowers -> R.drawable.ic_lock_open TootVisibility.PrivateFollowers -> R.drawable.ic_lock_open
@ -67,7 +67,7 @@ fun getVisibilityIconId(isMisskeyData: Boolean, visibility: TootVisibility): Int
TootVisibility.Limited -> R.drawable.ic_account_circle TootVisibility.Limited -> R.drawable.ic_account_circle
TootVisibility.Mutual -> R.drawable.ic_bidirectional TootVisibility.Mutual -> R.drawable.ic_bidirectional
} }
else -> when (visibility) { else -> when (this) {
TootVisibility.Public -> R.drawable.ic_public TootVisibility.Public -> R.drawable.ic_public
TootVisibility.UnlistedHome -> R.drawable.ic_lock_open TootVisibility.UnlistedHome -> R.drawable.ic_lock_open
TootVisibility.PrivateFollowers -> R.drawable.ic_lock TootVisibility.PrivateFollowers -> R.drawable.ic_lock
@ -87,19 +87,15 @@ fun getVisibilityIconId(isMisskeyData: Boolean, visibility: TootVisibility): Int
} }
} }
fun getVisibilityString( fun TootVisibility.getVisibilityString(isMisskeyData: Boolean): String {
context: Context, val isMisskey = when (PrefI.ipVisibilityStyle.value) {
isMisskeyData: Boolean,
visibility: TootVisibility,
): String {
val isMisskey = when (PrefI.ipVisibilityStyle()) {
PrefI.VS_MASTODON -> false PrefI.VS_MASTODON -> false
PrefI.VS_MISSKEY -> true PrefI.VS_MISSKEY -> true
else -> isMisskeyData else -> isMisskeyData
} }
return context.getString( return lazyContext.getString(
when { when {
isMisskey -> when (visibility) { isMisskey -> when (this) {
TootVisibility.Public -> R.string.visibility_public TootVisibility.Public -> R.string.visibility_public
TootVisibility.UnlistedHome -> R.string.visibility_home TootVisibility.UnlistedHome -> R.string.visibility_home
TootVisibility.PrivateFollowers -> R.string.visibility_followers TootVisibility.PrivateFollowers -> R.string.visibility_followers
@ -116,7 +112,7 @@ fun getVisibilityString(
TootVisibility.Limited -> R.string.visibility_limited TootVisibility.Limited -> R.string.visibility_limited
TootVisibility.Mutual -> R.string.visibility_mutual TootVisibility.Mutual -> R.string.visibility_mutual
} }
else -> when (visibility) { else -> when (this) {
TootVisibility.Public -> R.string.visibility_public TootVisibility.Public -> R.string.visibility_public
TootVisibility.UnlistedHome -> R.string.visibility_unlisted TootVisibility.UnlistedHome -> R.string.visibility_unlisted
TootVisibility.PrivateFollowers -> R.string.visibility_followers TootVisibility.PrivateFollowers -> R.string.visibility_followers
@ -144,8 +140,8 @@ fun getVisibilityCaption(
visibility: TootVisibility, visibility: TootVisibility,
): CharSequence { ): CharSequence {
val iconId = getVisibilityIconId(isMisskeyData, visibility) val iconId = visibility.getVisibilityIconId(isMisskeyData)
val sv = getVisibilityString(context, isMisskeyData, visibility) val sv = visibility.getVisibilityString(isMisskeyData)
val color = context.attrColor(R.attr.colorTextContent) val color = context.attrColor(R.attr.colorTextContent)
val sb = SpannableStringBuilder() val sb = SpannableStringBuilder()
@ -182,11 +178,11 @@ fun setFollowIcon(
alphaMultiplier: Float, alphaMultiplier: Float,
) { ) {
val colorFollowed = val colorFollowed =
PrefI.ipButtonFollowingColor(context.pref()).notZero() PrefI.ipButtonFollowingColor.value.notZero()
?: context.attrColor(R.attr.colorButtonAccentFollow) ?: context.attrColor(R.attr.colorButtonAccentFollow)
val colorFollowRequest = val colorFollowRequest =
PrefI.ipButtonFollowRequestColor(context.pref()).notZero() PrefI.ipButtonFollowRequestColor.value.notZero()
?: context.attrColor(R.attr.colorButtonAccentFollowRequest) ?: context.attrColor(R.attr.colorButtonAccentFollowRequest)
val colorError = context.attrColor(R.attr.colorRegexFilterError) val colorError = context.attrColor(R.attr.colorRegexFilterError)
@ -309,7 +305,7 @@ fun fixHorizontalPadding(v: View, dpDelta: Float = 12f) {
val widthDp = dm.widthPixels / dm.density val widthDp = dm.widthPixels / dm.density
if (widthDp >= 640f && v.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) { if (widthDp >= 640f && v.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
val padLr = (0.5f + dpDelta * dm.density).toInt() val padLr = (0.5f + dpDelta * dm.density).toInt()
when (PrefI.ipJustifyWindowContentPortrait()) { when (PrefI.ipJustifyWindowContentPortrait.value) {
PrefI.JWCP_START -> { PrefI.JWCP_START -> {
v.setPaddingRelative(padLr, padT, padLr + dm.widthPixels / 2, padB) v.setPaddingRelative(padLr, padT, padLr + dm.widthPixels / 2, padB)
return return
@ -338,7 +334,7 @@ fun fixHorizontalMargin(v: View) {
log.d("fixHorizontalMargin: orientation=$orientationString, w=${widthDp}dp, h=${dm.heightPixels / dm.density}") log.d("fixHorizontalMargin: orientation=$orientationString, w=${widthDp}dp, h=${dm.heightPixels / dm.density}")
if (widthDp >= 640f && v.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) { if (widthDp >= 640f && v.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
when (PrefI.ipJustifyWindowContentPortrait()) { when (PrefI.ipJustifyWindowContentPortrait.value) {
PrefI.JWCP_START -> { PrefI.JWCP_START -> {
lp.marginStart = 0 lp.marginStart = 0
lp.marginEnd = dm.widthPixels / 2 lp.marginEnd = dm.widthPixels / 2
@ -397,7 +393,7 @@ fun SpannableStringBuilder.appendMisskeyReaction(
emoji == null -> emoji == null ->
append("text") append("text")
PrefB.bpUseTwemoji(context) -> { PrefB.bpUseTwemoji.value -> {
val start = this.length val start = this.length
append(text) append(text)
val end = this.length val end = this.length
@ -414,9 +410,10 @@ fun SpannableStringBuilder.appendMisskeyReaction(
} }
fun Context.setSwitchColor(root: View?) { fun Context.setSwitchColor(root: View?) {
root ?: return
val colorBg = attrColor(R.attr.colorWindowBackground) val colorBg = attrColor(R.attr.colorWindowBackground)
val colorOff = attrColor(R.attr.colorSwitchOff) val colorOff = attrColor(R.attr.colorSwitchOff)
val colorOn = PrefI.ipSwitchOnColor() val colorOn = PrefI.ipSwitchOnColor.value
val colorDisabled = mixColor(colorBg, colorOff) val colorDisabled = mixColor(colorBg, colorOff)
@ -451,7 +448,7 @@ fun Context.setSwitchColor(root: View?) {
) )
) )
root?.scan { root.scan {
(it as? SwitchCompat)?.apply { (it as? SwitchCompat)?.apply {
thumbTintList = thumbStates thumbTintList = thumbStates
trackTintList = trackStates trackTintList = trackStates
@ -487,13 +484,14 @@ fun AppCompatActivity.setStatusBarColor(forceDark: Boolean = false) {
var c = when { var c = when {
forceDark -> Color.BLACK forceDark -> Color.BLACK
else -> PrefI.ipStatusBarColor.invoke().notZero() ?: attrColor(R.attr.colorPrimaryDark) else -> PrefI.ipStatusBarColor.value.notZero()
?: attrColor(R.attr.colorPrimaryDark)
} }
setStatusBarColorCompat(c) setStatusBarColorCompat(c)
c = when { c = when {
forceDark -> Color.BLACK forceDark -> Color.BLACK
else -> PrefI.ipNavigationBarColor() else -> PrefI.ipNavigationBarColor.value
} }
setNavigationBarColorCompat(c) setNavigationBarColorCompat(c)
} }

View File

@ -1,12 +1,13 @@
package jp.juggler.subwaytooter.action package jp.juggler.subwaytooter.action
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.runApiTask2 import jp.juggler.subwaytooter.api.runApiTask2
import jp.juggler.subwaytooter.api.showApiError import jp.juggler.subwaytooter.api.showApiError
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.matchHost import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonObject import jp.juggler.util.data.buildJsonObject
@ -22,7 +23,6 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
host: Host, host: Host,
instanceInfoArg: TootInstance? = null, instanceInfoArg: TootInstance? = null,
): SavedAccount? { ): SavedAccount? {
try { try {
suspend fun AppCompatActivity.getInstanceInfo(): TootInstance? { suspend fun AppCompatActivity.getInstanceInfo(): TootInstance? {
return try { return try {
@ -35,7 +35,7 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
val acct = Acct.parse("?", host) val acct = Acct.parse("?", host)
var account = SavedAccount.loadAccountByAcct(this, acct.ascii) var account = daoSavedAccount.loadAccountByAcct(acct)
if (account != null) return account if (account != null) return account
val instanceInfo = instanceInfoArg val instanceInfo = instanceInfoArg
@ -47,7 +47,7 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
put("acct", acct.username) // ローカルから参照した場合なのでshort acct put("acct", acct.username) // ローカルから参照した場合なのでshort acct
} }
val rowId = SavedAccount.insert( val rowId = daoSavedAccount.saveNew(
acct = acct.ascii, acct = acct.ascii,
host = host.ascii, host = host.ascii,
domain = instanceInfo.apDomain.ascii, domain = instanceInfo.apDomain.ascii,
@ -56,7 +56,7 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
misskeyVersion = instanceInfo.misskeyVersionMajor misskeyVersion = instanceInfo.misskeyVersionMajor
) )
account = SavedAccount.loadAccount(applicationContext, rowId) account = daoSavedAccount.loadAccount(rowId)
?: error("loadAccount returns null.") ?: error("loadAccount returns null.")
account.notification_follow = false account.notification_follow = false
@ -68,7 +68,7 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
account.notification_vote = false account.notification_vote = false
account.notification_post = false account.notification_post = false
account.notification_update = false account.notification_update = false
account.saveSetting() daoSavedAccount.saveSetting(account)
return account return account
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.e(ex, "addPseudoAccount failed.") log.e(ex, "addPseudoAccount failed.")
@ -77,22 +77,6 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
} }
} }
internal fun SavedAccount.saveUserRelation(src: TootRelationShip?): UserRelation? {
src ?: return null
val now = System.currentTimeMillis()
return UserRelation.save1Mastodon(now, db_id, src)
}
internal fun SavedAccount.saveUserRelationMisskey(
whoId: EntityId,
parser: TootParser,
): UserRelation? {
val now = System.currentTimeMillis()
val relation = parser.getMisskeyUserRelation(whoId)
UserRelation.save1Misskey(now, db_id, whoId.toString(), relation)
return relation
}
//// relationshipを取得 //// relationshipを取得
//internal fun loadRelation1Mastodon( //internal fun loadRelation1Mastodon(
// client : TootApiClient, // client : TootApiClient,

View File

@ -1,9 +1,7 @@
package jp.juggler.subwaytooter.action package jp.juggler.subwaytooter.action
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.os.Build import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.actmain.addColumn import jp.juggler.subwaytooter.actmain.addColumn
import jp.juggler.subwaytooter.actmain.afterAccountVerify import jp.juggler.subwaytooter.actmain.afterAccountVerify
@ -16,13 +14,11 @@ import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.dialog.* import jp.juggler.subwaytooter.dialog.*
import jp.juggler.subwaytooter.dialog.DlgCreateAccount.Companion.showUserCreateDialog import jp.juggler.subwaytooter.dialog.DlgCreateAccount.Companion.showUserCreateDialog
import jp.juggler.subwaytooter.dialog.LoginForm.Companion.showLoginForm import jp.juggler.subwaytooter.dialog.LoginForm.Companion.showLoginForm
import jp.juggler.subwaytooter.notification.APP_SERVER
import jp.juggler.subwaytooter.pref.* import jp.juggler.subwaytooter.pref.*
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.openBrowser import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchIO
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.encodePercent import jp.juggler.util.data.encodePercent
@ -32,7 +28,6 @@ import jp.juggler.util.network.toFormRequestBody
import jp.juggler.util.network.toPost import jp.juggler.util.network.toPost
import jp.juggler.util.ui.dismissSafe import jp.juggler.util.ui.dismissSafe
import kotlinx.coroutines.* import kotlinx.coroutines.*
import ru.gildor.coroutines.okhttp.await
private val log = LogCategory("Action_Account") private val log = LogCategory("Action_Account")
@ -195,53 +190,6 @@ fun ActMain.accessTokenPrompt(
) )
} }
fun AppCompatActivity.accountRemove(account: SavedAccount) {
// if account is default account of tablet mode,
// reset default.
val pref = pref()
if (account.db_id == PrefL.lpTabletTootDefaultAccount(pref)) {
pref.edit().put(PrefL.lpTabletTootDefaultAccount, -1L).apply()
}
account.delete()
appServerUnregister(applicationContext, account)
}
private fun appServerUnregister(context: Context, account: SavedAccount) {
launchIO {
try {
val installId = PrefDevice.from(context).getString(PrefDevice.KEY_INSTALL_ID, null)
if (installId?.isEmpty() != false) {
error("missing install_id")
}
val tag = account.notification_tag
if (tag?.isEmpty() != false) {
error("missing notification_tag")
}
val call = App1.ok_http_client.newCall(
"instance_url=${
"https://${account.apiHost.ascii}".encodePercent()
}&app_id=${
context.packageName.encodePercent()
}&tag=$tag"
.toFormRequestBody()
.toPost()
.url("$APP_SERVER/unregister")
.build()
)
val response = call.await()
if (!response.isSuccessful) {
log.e("appServerUnregister: $response")
}
} catch (ex: Throwable) {
log.e(ex, "appServerUnregister failed.")
}
}
}
// アカウント設定 // アカウント設定
fun ActMain.accountOpenSetting() { fun ActMain.accountOpenSetting() {
launchMain { launchMain {
@ -284,105 +232,3 @@ fun ActMain.accountResendConfirmMail(accessInfo: SavedAccount) {
}.show() }.show()
} }
//
fun accountListReorder(
src: List<SavedAccount>,
pickupHost: Host?,
filter: (SavedAccount) -> Boolean = { true },
): MutableList<SavedAccount> {
val listSameHost = java.util.ArrayList<SavedAccount>()
val listOtherHost = java.util.ArrayList<SavedAccount>()
for (a in src) {
if (!filter(a)) continue
when (pickupHost) {
null, a.apDomain, a.apiHost -> listSameHost
else -> listOtherHost
}.add(a)
}
SavedAccount.sort(listSameHost)
SavedAccount.sort(listOtherHost)
listSameHost.addAll(listOtherHost)
return listSameHost
}
// 疑似アカ以外のアカウントのリスト
fun Context.accountListNonPseudo(
pickupHost: Host?,
) = accountListReorder(
SavedAccount.loadAccountList(this),
pickupHost
) { !it.isPseudo }
// 条件でフィルタする。サーバ情報を読む場合がある。
suspend fun Context.accountListWithFilter(
pickupHost: Host?,
check: suspend (TootApiClient, SavedAccount) -> Boolean,
): MutableList<SavedAccount>? {
var resultList: MutableList<SavedAccount>? = null
runApiTask { client ->
supervisorScope {
resultList = SavedAccount.loadAccountList(this@accountListWithFilter)
.map {
async {
try {
if (check(client, it)) it else null
} catch (ex: Throwable) {
log.e(ex, "accountListWithFilter failed.")
null
}
}
}
.mapNotNull { it.await() }
.let { accountListReorder(it, pickupHost) }
}
if (client.isApiCancelled()) null else TootApiResult()
}
return resultList
}
suspend fun ActMain.accountListCanQuote(pickupHost: Host? = null) =
accountListWithFilter(pickupHost) { client, a ->
when {
client.isApiCancelled() -> false
a.isPseudo -> false
a.isMisskey -> true
else -> {
val (ti, ri) = TootInstance.getEx(client.copy(), account = a)
if (ti == null) {
ri?.error?.let { log.w(it) }
false
} else InstanceCapability.quote(ti)
}
}
}
suspend fun ActMain.accountListCanReaction(pickupHost: Host? = null) =
accountListWithFilter(pickupHost) { client, a ->
when {
client.isApiCancelled() -> false
a.isPseudo -> false
a.isMisskey -> true
else -> {
val (ti, ri) = TootInstance.getEx(client.copy(), account = a)
if (ti == null) {
ri?.error?.let { log.w(it) }
false
} else InstanceCapability.emojiReaction(a, ti)
}
}
}
suspend fun ActMain.accountListCanSeeMyReactions(pickupHost: Host? = null) =
accountListWithFilter(pickupHost) { client, a ->
when {
client.isApiCancelled() -> false
a.isPseudo -> false
else -> {
val (ti, ri) = TootInstance.getEx(client.copy(), account = a)
if (ti == null) {
ri?.error?.let { log.w(it) }
false
} else InstanceCapability.listMyReactions(a, ti)
}
}
}

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.action package jp.juggler.subwaytooter.action
import android.app.AlertDialog
import android.net.Uri import android.net.Uri
import jp.juggler.subwaytooter.ActColumnList import jp.juggler.subwaytooter.ActColumnList
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
@ -8,8 +7,10 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actmain.currentColumn import jp.juggler.subwaytooter.actmain.currentColumn
import jp.juggler.subwaytooter.actmain.handleOtherUri import jp.juggler.subwaytooter.actmain.handleOtherUri
import jp.juggler.subwaytooter.api.entity.TootApplication import jp.juggler.subwaytooter.api.entity.TootApplication
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.DlgOpenUrl import jp.juggler.subwaytooter.dialog.DlgOpenUrl
import jp.juggler.subwaytooter.table.MutedApp import jp.juggler.subwaytooter.table.daoMutedApp
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.ui.dismissSafe import jp.juggler.util.ui.dismissSafe
@ -18,22 +19,10 @@ fun ActMain.openColumnList() =
arColumnList.launch(ActColumnList.createIntent(this, currentColumn)) arColumnList.launch(ActColumnList.createIntent(this, currentColumn))
// アプリをミュートする // アプリをミュートする
fun ActMain.appMute( fun ActMain.appMute(application: TootApplication?) = launchAndShowError {
application: TootApplication?, application ?: return@launchAndShowError
confirmed: Boolean = false, confirm(R.string.mute_application_confirm, application.name)
) { daoMutedApp.save(application.name)
application ?: return
if (!confirmed) {
AlertDialog.Builder(this)
.setMessage(getString(R.string.mute_application_confirm, application.name))
.setPositiveButton(R.string.ok) { _, _ ->
appMute(application, confirmed = true)
}
.setNegativeButton(R.string.cancel, null)
.show()
return
}
MutedApp.save(application.name)
appState.onMuteUpdated() appState.onMuteUpdated()
showToast(false, R.string.app_was_muted) showToast(false, R.string.app_was_muted)
} }

View File

@ -17,8 +17,10 @@ import jp.juggler.subwaytooter.column.findStatus
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.getVisibilityCaption import jp.juggler.subwaytooter.getVisibilityCaption
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.accountListNonPseudo
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.emptyCallback import jp.juggler.subwaytooter.util.emptyCallback
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
@ -212,7 +214,7 @@ private class BoostImpl(
visibility == TootVisibility.PrivateFollowers -> R.string.confirm_private_boost_from visibility == TootVisibility.PrivateFollowers -> R.string.confirm_private_boost_from
else -> R.string.confirm_boost_from else -> R.string.confirm_boost_from
}, },
AcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
), ),
when (bSet) { when (bSet) {
true -> accessInfo.confirm_boost true -> accessInfo.confirm_boost
@ -223,7 +225,7 @@ private class BoostImpl(
true -> accessInfo.confirm_boost = newConfirmEnabled true -> accessInfo.confirm_boost = newConfirmEnabled
else -> accessInfo.confirm_unboost = newConfirmEnabled else -> accessInfo.confirm_unboost = newConfirmEnabled
} }
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
activity.reloadAccountSetting(accessInfo) activity.reloadAccountSetting(accessInfo)
} }
} }
@ -288,7 +290,7 @@ fun ActMain.boostFromAnotherAccount(
if (isPrivateToot) { if (isPrivateToot) {
val list = ArrayList<SavedAccount>() val list = ArrayList<SavedAccount>()
for (a in SavedAccount.loadAccountList(applicationContext)) { for (a in daoSavedAccount.loadAccountList()) {
if (a.acct == statusOwner) list.add(a) if (a.acct == statusOwner) list.add(a)
} }
if (list.isEmpty()) { if (list.isEmpty()) {

View File

@ -10,11 +10,14 @@ import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.findStatus import jp.juggler.subwaytooter.column.findStatus
import jp.juggler.subwaytooter.columnviewholder.ItemListAdapter import jp.juggler.subwaytooter.columnviewholder.ItemListAdapter
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.table.sortedByNickname
import jp.juggler.subwaytooter.util.matchHost import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.subwaytooter.util.openCustomTab import jp.juggler.subwaytooter.util.openCustomTab
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -244,122 +247,126 @@ fun ActMain.conversationOtherInstance(
statusIdAccess: EntityId? = null, statusIdAccess: EntityId? = null,
isReference: Boolean = false, isReference: Boolean = false,
) { ) {
val activity = this val activity = this
launchAndShowError {
actionsDialog(getString(R.string.open_status_from)) {
val dialog = ActionsDialog() val hostOriginal = Host.parse(urlArg.toUri().authority ?: "")
val hostOriginal = Host.parse(urlArg.toUri().authority ?: "") // 選択肢:ブラウザで表示する
action(getString(R.string.open_web_on_host, hostOriginal.pretty)) {
openCustomTab(urlArg)
}
// 選択肢:ブラウザで表示する // トゥートの投稿元タンスにあるアカウント
dialog.addAction( val localAccountList = ArrayList<SavedAccount>()
getString(
R.string.open_web_on_host,
hostOriginal.pretty
)
) { openCustomTab(urlArg) }
// トゥートの投稿元タンスにあるアカウント // TLを読んだタンスにあるアカウント
val localAccountList = ArrayList<SavedAccount>() val accessAccountList = ArrayList<SavedAccount>()
// TLを読んだタンスにあるアカウント // その他のタンスにあるアカウント
val accessAccountList = ArrayList<SavedAccount>() val otherAccountList = ArrayList<SavedAccount>()
// その他のタンスにあるアカウント for (a in daoSavedAccount.loadAccountList()) {
val otherAccountList = ArrayList<SavedAccount>()
for (a in SavedAccount.loadAccountList(applicationContext)) { // 疑似アカウントは後でまとめて処理する
if (a.isPseudo) continue
// 疑似アカウントは後でまとめて処理する if (isReference && TootInstance.getCached(a)?.canUseReference != true) continue
if (a.isPseudo) continue
if (isReference && TootInstance.getCached(a)?.canUseReference != true) continue if (statusIdOriginal != null && a.matchHost(hostOriginal)) {
// アクセス情報ステータスID でアクセスできるなら
// 同タンスのアカウントならステータスIDの変換なしに表示できる
localAccountList.add(a)
} else if (statusIdAccess != null && a.matchHost(hostAccess)) {
// 既に変換済みのステータスIDがあるなら、そのアカウントでもステータスIDの変換は必要ない
accessAccountList.add(a)
} else {
// 別タンスでも実アカウントなら検索APIでステータスIDを変換できる
otherAccountList.add(a)
}
}
if (statusIdOriginal != null && a.matchHost(hostOriginal)) { // 参照の場合、status URLから/references を除去しないとURLでの検索ができない
// アクセス情報ステータスID でアクセスできるなら val url = when {
// 同タンスのアカウントならステータスIDの変換なしに表示できる isReference -> """/references\z""".toRegex().replace(urlArg, "")
localAccountList.add(a) else -> urlArg
} else if (statusIdAccess != null && a.matchHost(hostAccess)) { }
// 既に変換済みのステータスIDがあるなら、そのアカウントでもステータスIDの変換は必要ない
accessAccountList.add(a)
} else {
// 別タンスでも実アカウントなら検索APIでステータスIDを変換できる
otherAccountList.add(a)
}
}
// 参照の場合、status URLから/references を除去しないとURLでの検索ができない // 同タンスのアカウントがないなら、疑似アカウントで開く選択肢
val url = when { if (localAccountList.isEmpty()) {
isReference -> """/references\z""".toRegex().replace(urlArg, "") if (statusIdOriginal != null) {
else -> urlArg action(
} getString(R.string.open_in_pseudo_account, "?@${hostOriginal.pretty}")
) {
// 同タンスのアカウントがないなら、疑似アカウントで開く選択肢 launchMain {
if (localAccountList.isEmpty()) { addPseudoAccount(hostOriginal)?.let { sa ->
if (statusIdOriginal != null) { conversationLocal(
dialog.addAction( pos,
getString(R.string.open_in_pseudo_account, "?@${hostOriginal.pretty}") sa,
) { statusIdOriginal,
launchMain { isReference = isReference
addPseudoAccount(hostOriginal)?.let { sa -> )
conversationLocal(pos, sa, statusIdOriginal, isReference = isReference) }
}
}
} else {
action(
getString(R.string.open_in_pseudo_account, "?@${hostOriginal.pretty}")
) {
launchMain {
addPseudoAccount(hostOriginal)?.let { sa ->
conversationRemote(pos, sa, url)
}
}
} }
} }
} }
} else {
dialog.addAction( // ローカルアカウント
getString(R.string.open_in_pseudo_account, "?@${hostOriginal.pretty}") if (statusIdOriginal != null) {
) { for (a in localAccountList.sortedByNickname()) {
launchMain { action(
addPseudoAccount(hostOriginal)?.let { sa -> daoAcctColor.getStringWithNickname(
conversationRemote(pos, sa, url) activity,
R.string.open_in_account,
a.acct
)
) {
conversationLocal(pos, a, statusIdOriginal, isReference = isReference)
} }
} }
} }
// アクセスしたアカウント
if (statusIdAccess != null) {
for (a in accessAccountList.sortedByNickname()) {
action(
daoAcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) {
conversationLocal(pos, a, statusIdAccess, isReference = isReference)
}
}
}
// その他の実アカウント
for (a in otherAccountList.sortedByNickname()) {
action(
daoAcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) {
conversationRemote(pos, a, url)
}
}
} }
} }
// ローカルアカウント
if (statusIdOriginal != null) {
SavedAccount.sort(localAccountList)
for (a in localAccountList) {
dialog.addAction(
AcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { conversationLocal(pos, a, statusIdOriginal, isReference = isReference) }
}
}
// アクセスしたアカウント
if (statusIdAccess != null) {
SavedAccount.sort(accessAccountList)
for (a in accessAccountList) {
dialog.addAction(
AcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { conversationLocal(pos, a, statusIdAccess, isReference = isReference) }
}
}
// その他の実アカウント
SavedAccount.sort(otherAccountList)
for (a in otherAccountList) {
dialog.addAction(
AcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { conversationRemote(pos, a, url) }
}
dialog.show(activity, activity.getString(R.string.open_status_from))
} }
// リモートかもしれない会話の流れを表示する // リモートかもしれない会話の流れを表示する
@ -466,65 +473,59 @@ fun ActMain.conversationFromTootsearch(
) { ) {
statusArg ?: return statusArg ?: return
// step2: 選択したアカウントで投稿を検索して返信元の投稿のIDを調べる
fun step2(a: SavedAccount) = launchMain {
var tmp: TootStatus? = null
runApiTask(a) { client ->
val (result, status) = client.syncStatus(a, statusArg)
tmp = status
result
}?.let { result ->
val status = tmp
val replyId = status?.in_reply_to_id
when {
status == null -> showToast(true, result.error ?: "?")
replyId == null -> showToast(true, "showReplyTootsearch: in_reply_to_id is null")
else -> conversationLocal(pos, a, replyId)
}
}
}
// step 1: choose account // step 1: choose account
val host = statusArg.account.apDomain val host = statusArg.account.apDomain
val localAccountList = ArrayList<SavedAccount>() val localAccountList = ArrayList<SavedAccount>()
val otherAccountList = ArrayList<SavedAccount>() val otherAccountList = ArrayList<SavedAccount>()
for (a in daoSavedAccount.loadAccountList()) {
for (a in SavedAccount.loadAccountList(this)) { when {
// 検索APIはログイン必須なので疑似アカウントは使えない
// 検索APIはログイン必須なので疑似アカウントは使えない a.isPseudo -> continue
if (a.isPseudo) continue a.matchHost(host) -> localAccountList.add(a)
else -> otherAccountList.add(a)
if (a.matchHost(host)) {
localAccountList.add(a)
} else {
otherAccountList.add(a)
} }
} }
val dialog = ActionsDialog() val activity = this
launchAndShowError {
SavedAccount.sort(localAccountList) // step2: 選択したアカウントで投稿を検索して返信元の投稿のIDを調べる
for (a in localAccountList) { suspend fun step2(a: SavedAccount) {
dialog.addAction( var tmp: TootStatus? = null
AcctColor.getStringWithNickname( runApiTask(a) { client ->
this, val (result, status) = client.syncStatus(a, statusArg)
R.string.open_in_account, tmp = status
a.acct result
) }?.let { result ->
) { step2(a) } val status = tmp
val replyId = status?.in_reply_to_id
when {
status == null -> showToast(true, result.error ?: "?")
replyId == null -> showToast(true, "showReplyTootsearch: in_reply_to_id is null")
else -> conversationLocal(pos, a, replyId)
}
}
}
actionsDialog(getString(R.string.open_status_from)) {
for (a in localAccountList.sortedByNickname()) {
action(
daoAcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { step2(a) }
}
for (a in otherAccountList.sortedByNickname()) {
action(
daoAcctColor.getStringWithNickname(
activity,
R.string.open_in_account,
a.acct
)
) { step2(a) }
}
}
} }
SavedAccount.sort(otherAccountList)
for (a in otherAccountList) {
dialog.addAction(
AcctColor.getStringWithNickname(
this,
R.string.open_in_account,
a.acct
)
) { step2(a) }
}
dialog.show(this, getString(R.string.open_status_from))
} }

View File

@ -6,8 +6,8 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TootFilter import jp.juggler.subwaytooter.api.entity.TootFilter
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.onFilterDeleted import jp.juggler.subwaytooter.column.onFilterDeleted
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
@ -17,15 +17,17 @@ import okhttp3.Request
fun ActMain.openFilterMenu(accessInfo: SavedAccount, item: TootFilter?) { fun ActMain.openFilterMenu(accessInfo: SavedAccount, item: TootFilter?) {
item ?: return item ?: return
val activity = this
val ad = ActionsDialog() launchAndShowError {
ad.addAction(getString(R.string.edit)) { actionsDialog(getString(R.string.filter_of, item.displayString)) {
ActKeywordFilter.open(this, accessInfo, item.id) action(getString(R.string.edit)) {
ActKeywordFilter.open(activity, accessInfo, item.id)
}
action(getString(R.string.delete)) {
filterDelete(accessInfo, item)
}
}
} }
ad.addAction(getString(R.string.delete)) {
filterDelete(accessInfo, item)
}
ad.show(this, getString(R.string.filter_of, item.displayString))
} }
fun ActMain.filterDelete( fun ActMain.filterDelete(

View File

@ -12,9 +12,7 @@ import jp.juggler.subwaytooter.column.fireRebindAdapterItems
import jp.juggler.subwaytooter.column.removeUser import jp.juggler.subwaytooter.column.removeUser
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
@ -81,7 +79,7 @@ fun ActMain.clickFollowRequestAccept(
accept -> R.string.follow_accept_confirm accept -> R.string.follow_accept_confirm
else -> R.string.follow_deny_confirm else -> R.string.follow_deny_confirm
}, },
AcctColor.getNickname(accessInfo, who) daoAcctColor.getNickname(accessInfo, who)
) )
followRequestAuthorize(accessInfo, whoRef, accept) followRequestAuthorize(accessInfo, whoRef, accept)
} }
@ -152,12 +150,12 @@ fun ActMain.follow(
getString( getString(
R.string.confirm_follow_request_who_from, R.string.confirm_follow_request_who_from,
whoRef.decoded_display_name, whoRef.decoded_display_name,
AcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
), ),
accessInfo.confirm_follow_locked, accessInfo.confirm_follow_locked,
) { newConfirmEnabled -> ) { newConfirmEnabled ->
accessInfo.confirm_follow_locked = newConfirmEnabled accessInfo.confirm_follow_locked = newConfirmEnabled
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
activity.reloadAccountSetting(accessInfo) activity.reloadAccountSetting(accessInfo)
} }
} else if (bFollow) { } else if (bFollow) {
@ -165,12 +163,12 @@ fun ActMain.follow(
getString( getString(
R.string.confirm_follow_who_from, R.string.confirm_follow_who_from,
whoRef.decoded_display_name, whoRef.decoded_display_name,
AcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
), ),
accessInfo.confirm_follow accessInfo.confirm_follow
) { newConfirmEnabled -> ) { newConfirmEnabled ->
accessInfo.confirm_follow = newConfirmEnabled accessInfo.confirm_follow = newConfirmEnabled
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
activity.reloadAccountSetting(accessInfo) activity.reloadAccountSetting(accessInfo)
} }
} else { } else {
@ -178,12 +176,12 @@ fun ActMain.follow(
getString( getString(
R.string.confirm_unfollow_who_from, R.string.confirm_unfollow_who_from,
whoRef.decoded_display_name, whoRef.decoded_display_name,
AcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
), ),
accessInfo.confirm_unfollow accessInfo.confirm_unfollow
) { newConfirmEnabled -> ) { newConfirmEnabled ->
accessInfo.confirm_unfollow = newConfirmEnabled accessInfo.confirm_unfollow = newConfirmEnabled
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
activity.reloadAccountSetting(accessInfo) activity.reloadAccountSetting(accessInfo)
} }
} }
@ -235,9 +233,9 @@ fun ActMain.follow(
)?.also { result -> )?.also { result ->
fun saveFollow(f: Boolean) { fun saveFollow(f: Boolean) {
val ur = UserRelation.load(accessInfo.db_id, userId) val ur = daoUserRelation.load(accessInfo.db_id, userId)
ur.following = f ur.following = f
UserRelation.save1Misskey( daoUserRelation.save1Misskey(
System.currentTimeMillis(), System.currentTimeMillis(),
accessInfo.db_id, accessInfo.db_id,
userId.toString(), userId.toString(),
@ -264,7 +262,7 @@ fun ActMain.follow(
"".toFormRequestBody().toPost() "".toFormRequestBody().toPost()
)?.also { result -> )?.also { result ->
val newRelation = parseItem(::TootRelationShip, parser, result.jsonObject) val newRelation = parseItem(::TootRelationShip, parser, result.jsonObject)
resultRelation = accessInfo.saveUserRelation(newRelation) resultRelation = daoUserRelation.saveUserRelation(accessInfo, newRelation)
} }
} }
}?.let { result -> }?.let { result ->
@ -313,26 +311,26 @@ private fun ActMain.followRemote(
confirm( confirm(
getString( getString(
R.string.confirm_follow_request_who_from, R.string.confirm_follow_request_who_from,
AcctColor.getNickname(acct), daoAcctColor.getNickname(acct),
AcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
), ),
accessInfo.confirm_follow_locked, accessInfo.confirm_follow_locked,
) { newConfirmEnabled -> ) { newConfirmEnabled ->
accessInfo.confirm_follow_locked = newConfirmEnabled accessInfo.confirm_follow_locked = newConfirmEnabled
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
reloadAccountSetting(accessInfo) reloadAccountSetting(accessInfo)
} }
} else { } else {
confirm( confirm(
getString( getString(
R.string.confirm_follow_who_from, R.string.confirm_follow_who_from,
AcctColor.getNickname(acct), daoAcctColor.getNickname(acct),
AcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
), ),
accessInfo.confirm_follow accessInfo.confirm_follow
) { newConfirmEnabled -> ) { newConfirmEnabled ->
accessInfo.confirm_follow = newConfirmEnabled accessInfo.confirm_follow = newConfirmEnabled
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
reloadAccountSetting(accessInfo) reloadAccountSetting(accessInfo)
} }
} }
@ -357,12 +355,16 @@ private fun ActMain.followRemote(
result?.error?.contains("already not following") == true result?.error?.contains("already not following") == true
) { ) {
// DBから読み直して値を変更する // DBから読み直して値を変更する
resultRelation = UserRelation.load(accessInfo.db_id, userId) resultRelation = daoUserRelation.load(accessInfo.db_id, userId)
.apply { following = true } .apply { following = true }
} else { } else {
// parserに残ってるRelationをDBに保存する // parserに残ってるRelationをDBに保存する
parser.account(result?.jsonObject)?.let { parser.account(result?.jsonObject)?.let {
resultRelation = accessInfo.saveUserRelationMisskey(it.id, parser) resultRelation = daoUserRelation.saveUserRelationMisskey(
accessInfo,
it.id,
parser
)
} }
} }
} }
@ -372,7 +374,7 @@ private fun ActMain.followRemote(
"".toFormRequestBody().toPost() "".toFormRequestBody().toPost()
)?.also { result -> )?.also { result ->
parseItem(::TootRelationShip, parser, result.jsonObject)?.let { parseItem(::TootRelationShip, parser, result.jsonObject)?.let {
resultRelation = accessInfo.saveUserRelation(it) resultRelation = daoUserRelation.saveUserRelation(accessInfo, it)
} }
} }
} }
@ -469,7 +471,11 @@ fun ActMain.followRequestAuthorize(
val user = parser.account(result?.jsonObject) val user = parser.account(result?.jsonObject)
if (user != null) { if (user != null) {
// parserに残ってるRelationをDBに保存する // parserに残ってるRelationをDBに保存する
accessInfo.saveUserRelationMisskey(user.id, parser) daoUserRelation.saveUserRelationMisskey(
accessInfo,
user.id,
parser
)
} }
// 読めなくてもエラー処理は行わない // 読めなくてもエラー処理は行わない
} }
@ -481,7 +487,7 @@ fun ActMain.followRequestAuthorize(
// Mastodon 3.0.0 から更新されたリレーションを返す // Mastodon 3.0.0 から更新されたリレーションを返す
// https//github.com/tootsuite/mastodon/pull/11800 // https//github.com/tootsuite/mastodon/pull/11800
val newRelation = parseItem(::TootRelationShip, parser, result.jsonObject) val newRelation = parseItem(::TootRelationShip, parser, result.jsonObject)
accessInfo.saveUserRelation(newRelation) daoUserRelation.saveUserRelation(accessInfo, newRelation)
// 読めなくてもエラー処理は行わない // 読めなくてもエラー処理は行わない
} }
} }
@ -543,7 +549,7 @@ fun ActMain.followRequestDelete(
confirm( confirm(
R.string.confirm_cancel_follow_request_who_from, R.string.confirm_cancel_follow_request_who_from,
whoRef.decoded_display_name, whoRef.decoded_display_name,
AcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
) )
} }
@ -572,7 +578,12 @@ fun ActMain.followRequestDelete(
)?.also { result -> )?.also { result ->
parser.account(result.jsonObject)?.let { parser.account(result.jsonObject)?.let {
// parserに残ってるRelationをDBに保存する // parserに残ってるRelationをDBに保存する
resultRelation = accessInfo.saveUserRelationMisskey(it.id, parser) resultRelation = daoUserRelation.saveUserRelationMisskey(
accessInfo,
it.id,
parser
)
} }
} }
} }

View File

@ -13,9 +13,9 @@ import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.onListListUpdated import jp.juggler.subwaytooter.column.onListListUpdated
import jp.juggler.subwaytooter.column.onListNameUpdated import jp.juggler.subwaytooter.column.onListNameUpdated
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.DlgTextInput import jp.juggler.subwaytooter.dialog.DlgTextInput
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
@ -36,26 +36,28 @@ fun ActMain.clickListTl(pos: Int, accessInfo: SavedAccount, item: TimelineItem?)
fun ActMain.clickListMoreButton(pos: Int, accessInfo: SavedAccount, item: TimelineItem?) { fun ActMain.clickListMoreButton(pos: Int, accessInfo: SavedAccount, item: TimelineItem?) {
when (item) { when (item) {
is TootList -> { is TootList -> {
ActionsDialog() launchAndShowError {
.addAction(getString(R.string.list_timeline)) { actionsDialog(item.title) {
addColumn(pos, accessInfo, ColumnType.LIST_TL, item.id) action(getString(R.string.list_timeline)) {
addColumn(pos, accessInfo, ColumnType.LIST_TL, item.id)
}
action(getString(R.string.list_member)) {
addColumn(
false,
pos,
accessInfo,
ColumnType.LIST_MEMBER,
item.id
)
}
action(getString(R.string.rename)) {
listRename(accessInfo, item)
}
action(getString(R.string.delete)) {
listDelete(accessInfo, item)
}
} }
.addAction(getString(R.string.list_member)) { }
addColumn(
false,
pos,
accessInfo,
ColumnType.LIST_MEMBER,
item.id
)
}
.addAction(getString(R.string.rename)) {
listRename(accessInfo, item)
}
.addAction(getString(R.string.delete)) {
listDelete(accessInfo, item)
}
.show(this, item.title)
} }
is MisskeyAntenna -> { is MisskeyAntenna -> {

View File

@ -12,6 +12,7 @@ import jp.juggler.subwaytooter.api.syncAccountByAcct
import jp.juggler.subwaytooter.column.onListMemberUpdated import jp.juggler.subwaytooter.column.onListMemberUpdated
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
@ -71,7 +72,8 @@ fun ActMain.listMemberAdd(
"".toFormRequestBody().toPost() "".toFormRequestBody().toPost()
) ?: return@runApiTask null ) ?: return@runApiTask null
val relation = accessInfo.saveUserRelation( val relation = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject) parseItem(::TootRelationShip, parser, result.jsonObject)
) ?: return@runApiTask TootApiResult("parse error.") ) ?: return@runApiTask TootApiResult("parse error.")

View File

@ -16,10 +16,13 @@ import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.api.syncStatus import jp.juggler.subwaytooter.api.syncStatus
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.PrefS import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.accountListCanQuote
import jp.juggler.subwaytooter.table.accountListNonPseudo
import jp.juggler.subwaytooter.util.matchHost import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
@ -37,7 +40,7 @@ fun ActPost.saveWindowSize() {
// WindowMetrics#getBounds() the window size including all system bar areas // WindowMetrics#getBounds() the window size including all system bar areas
windowManager?.currentWindowMetrics?.bounds?.let { bounds -> windowManager?.currentWindowMetrics?.bounds?.let { bounds ->
log.d("API=${Build.VERSION.SDK_INT}, WindowMetrics#getBounds() $bounds") log.d("API=${Build.VERSION.SDK_INT}, WindowMetrics#getBounds() $bounds")
PrefDevice.savePostWindowBound(this, bounds.width(), bounds.height()) prefDevice.savePostWindowBound(bounds.width(), bounds.height())
} }
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -45,7 +48,7 @@ fun ActPost.saveWindowSize() {
val dm = DisplayMetrics() val dm = DisplayMetrics()
display.getMetrics(dm) display.getMetrics(dm)
log.d("API=${Build.VERSION.SDK_INT}, displayMetrics=${dm.widthPixels},${dm.heightPixels}") log.d("API=${Build.VERSION.SDK_INT}, displayMetrics=${dm.widthPixels},${dm.heightPixels}")
PrefDevice.savePostWindowBound(this, dm.widthPixels, dm.heightPixels) prefDevice.savePostWindowBound(dm.widthPixels, dm.heightPixels)
} }
} }
} }
@ -75,8 +78,8 @@ fun ActMain.openActPostImpl(
scheduledStatus: TootScheduled? = null, scheduledStatus: TootScheduled? = null,
) { ) {
val useManyWindow = PrefB.bpManyWindowPost(pref) val useManyWindow = PrefB.bpManyWindowPost.value
val useMultiWindow = useManyWindow || PrefB.bpMultiWindowPost(pref) val useMultiWindow = useManyWindow || PrefB.bpMultiWindowPost.value
val intent = ActPost.createIntent( val intent = ActPost.createIntent(
context = this, context = this,
@ -99,7 +102,9 @@ fun ActMain.openActPostImpl(
ActPost.refActPost?.get() ActPost.refActPost?.get()
?.takeIf { it.isLiveActivity } ?.takeIf { it.isLiveActivity }
?.let { ?.let {
it.updateText(intent) launchAndShowError {
it.updateText(intent)
}
return return
} }
} }
@ -108,11 +113,10 @@ fun ActMain.openActPostImpl(
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
var options = ActivityOptionsCompat.makeBasic() var options = ActivityOptionsCompat.makeBasic()
PrefDevice.loadPostWindowBound(this) prefDevice.loadPostWindowBound()?.let {
?.let { log.d("ActPost launchBounds $it")
log.d("ActPost launchBounds $it") options = options.setLaunchBounds(it)
options = options.setLaunchBounds(it) }
}
arActPost.launch(intent, options) arActPost.launch(intent, options)
} }
@ -254,7 +258,7 @@ fun ActMain.quoteFromAnotherAccount(
fun ActMain.quoteName(who: TootAccount) { fun ActMain.quoteName(who: TootAccount) {
var sv = who.display_name var sv = who.display_name
try { try {
val fmt = PrefS.spQuoteNameFormat(pref) val fmt = PrefS.spQuoteNameFormat.value
if (fmt.contains("%1\$s")) { if (fmt.contains("%1\$s")) {
sv = String.format(Locale.getDefault(), fmt, sv) sv = String.format(Locale.getDefault(), fmt, sv)
} }

View File

@ -18,8 +18,10 @@ import jp.juggler.subwaytooter.dialog.launchEmojiPicker
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.accountListCanReaction
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
@ -122,11 +124,15 @@ fun ActMain.reactionAdd(
) )
val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg) val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg)
confirm( confirm(
getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(accessInfo)), getString(
R.string.confirm_reaction,
emojiSpan,
daoAcctColor.getNickname(accessInfo)
),
accessInfo.confirm_reaction, accessInfo.confirm_reaction,
) { newConfirmEnabled -> ) { newConfirmEnabled ->
accessInfo.confirm_reaction = newConfirmEnabled accessInfo.confirm_reaction = newConfirmEnabled
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
} }
} }
@ -336,16 +342,20 @@ private fun ActMain.reactionWithoutUi(
isCustomEmoji && url?.likePleromaStatusUrl() == true -> confirm( isCustomEmoji && url?.likePleromaStatusUrl() == true -> confirm(
R.string.confirm_reaction_to_pleroma, R.string.confirm_reaction_to_pleroma,
emojiSpan, emojiSpan,
AcctColor.getNickname(accessInfo), daoAcctColor.getNickname(accessInfo),
resolvedStatus.account.acct.host?.pretty ?: "(null)" resolvedStatus.account.acct.host?.pretty ?: "(null)"
) )
else -> confirm( else -> confirm(
getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(accessInfo)), getString(
R.string.confirm_reaction,
emojiSpan,
daoAcctColor.getNickname(accessInfo)
),
accessInfo.confirm_reaction, accessInfo.confirm_reaction,
) { newConfirmEnabled -> ) { newConfirmEnabled ->
accessInfo.confirm_reaction = newConfirmEnabled accessInfo.confirm_reaction = newConfirmEnabled
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
} }
} }

View File

@ -12,11 +12,13 @@ import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.TootScheduled import jp.juggler.subwaytooter.api.entity.TootScheduled
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.column.* import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.accountListNonPseudo
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.emptyCallback import jp.juggler.subwaytooter.util.emptyCallback
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
@ -85,18 +87,20 @@ fun ActMain.clickFavourite(accessInfo: SavedAccount, status: TootStatus, willToa
} }
fun ActMain.clickScheduledToot(accessInfo: SavedAccount, item: TootScheduled, column: Column) { fun ActMain.clickScheduledToot(accessInfo: SavedAccount, item: TootScheduled, column: Column) {
ActionsDialog() launchAndShowError {
.addAction(getString(R.string.edit)) { actionsDialog {
scheduledPostEdit(accessInfo, item) action(getString(R.string.edit)) {
} scheduledPostEdit(accessInfo, item)
.addAction(getString(R.string.delete)) { }
launchAndShowError { action(getString(R.string.delete)) {
scheduledPostDelete(accessInfo, item) launchAndShowError {
column.onScheduleDeleted(item) scheduledPostDelete(accessInfo, item)
showToast(false, R.string.scheduled_post_deleted) column.onScheduleDeleted(item)
showToast(false, R.string.scheduled_post_deleted)
}
} }
} }
.show(this) }
} }
fun ActMain.launchActText(intent: Intent) = arActText.launch(intent) fun ActMain.launchActText(intent: Intent) = arActText.launch(intent)
@ -125,7 +129,7 @@ fun ActMain.favourite(
true -> R.string.confirm_favourite_from true -> R.string.confirm_favourite_from
else -> R.string.confirm_unfavourite_from else -> R.string.confirm_unfavourite_from
}, },
AcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
), ),
when (bSet) { when (bSet) {
true -> accessInfo.confirm_favourite true -> accessInfo.confirm_favourite
@ -136,7 +140,7 @@ fun ActMain.favourite(
true -> accessInfo.confirm_favourite = newConfirmEnabled true -> accessInfo.confirm_favourite = newConfirmEnabled
else -> accessInfo.confirm_unfavourite = newConfirmEnabled else -> accessInfo.confirm_unfavourite = newConfirmEnabled
} }
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
reloadAccountSetting(accessInfo) reloadAccountSetting(accessInfo)
} }
} }
@ -294,12 +298,12 @@ fun ActMain.bookmark(
confirm( confirm(
getString( getString(
R.string.confirm_unbookmark_from, R.string.confirm_unbookmark_from,
AcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
), ),
accessInfo.confirm_unbookmark accessInfo.confirm_unbookmark
) { newConfirmEnabled -> ) { newConfirmEnabled ->
accessInfo.confirm_unbookmark = newConfirmEnabled accessInfo.confirm_unbookmark = newConfirmEnabled
accessInfo.saveSetting() daoSavedAccount.saveSetting(accessInfo)
reloadAccountSetting(accessInfo) reloadAccountSetting(accessInfo)
} }
} }

View File

@ -11,12 +11,15 @@ import jp.juggler.subwaytooter.api.entity.TootTag
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.onTagFollowChanged import jp.juggler.subwaytooter.column.onTagFollowChanged
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.table.sortedByNickname
import jp.juggler.subwaytooter.util.matchHost import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.subwaytooter.util.openCustomTab import jp.juggler.subwaytooter.util.openCustomTab
import jp.juggler.util.coroutine.AppDispatchers import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.encodePercent import jp.juggler.util.data.encodePercent
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -57,23 +60,21 @@ fun ActMain.tagDialog(
) { ) {
val activity = this val activity = this
val tagWithSharp = "#$tagWithoutSharp" val tagWithSharp = "#$tagWithoutSharp"
launchMain { launchAndShowError {
try { actionsDialog(tagWithSharp) {
action(getString(R.string.open_hashtag_column)) {
val d = ActionsDialog() tagTimelineFromAccount(
.addAction(getString(R.string.open_hashtag_column)) { pos,
tagTimelineFromAccount( url,
pos, host,
url, tagWithoutSharp
host, )
tagWithoutSharp }
)
}
// 投稿者別タグTL // 投稿者別タグTL
if (whoAcct != null) { if (whoAcct != null) {
d.addAction( action(
AcctColor.getStringWithNickname( daoAcctColor.getStringWithNickname(
activity, activity,
R.string.open_hashtag_from_account, R.string.open_hashtag_from_account,
whoAcct whoAcct
@ -89,13 +90,13 @@ fun ActMain.tagDialog(
} }
} }
d.addAction(getString(R.string.open_in_browser)) { openCustomTab(url) } action(getString(R.string.open_in_browser)) {
.addAction( openCustomTab(url)
getString( }
R.string.quote_hashtag_of,
tagWithSharp action(getString(R.string.quote_hashtag_of, tagWithSharp)) {
) openPost("$tagWithSharp ")
) { openPost("$tagWithSharp ") } }
if (tagList != null && tagList.size > 1) { if (tagList != null && tagList.size > 1) {
val sb = StringBuilder() val sb = StringBuilder()
@ -104,11 +105,8 @@ fun ActMain.tagDialog(
sb.append(s) sb.append(s)
} }
val tagAll = sb.toString() val tagAll = sb.toString()
d.addAction( action(
getString( getString(R.string.quote_all_hashtag_of, tagAll)
R.string.quote_all_hashtag_of,
tagAll
)
) { openPost("$tagAll ") } ) { openPost("$tagAll ") }
} }
@ -118,10 +116,12 @@ fun ActMain.tagDialog(
if (tag == null) { if (tag == null) {
val result = runApiTask(accessInfo) { client -> val result = runApiTask(accessInfo) { client ->
client.request("/api/v1/tags/${tagWithoutSharp.encodePercent()}") client.request("/api/v1/tags/${tagWithoutSharp.encodePercent()}")
} ?: return@launchMain //cancelled. }
TootParser(activity, accessInfo) if (result != null) {
.tag(result.jsonObject) TootParser(activity, accessInfo)
?.let { tag = it } .tag(result.jsonObject)
?.let { tag = it }
}
} }
val toggle = !(tag?.following ?: false) val toggle = !(tag?.following ?: false)
@ -129,14 +129,10 @@ fun ActMain.tagDialog(
true -> R.string.follow_hashtag_of true -> R.string.follow_hashtag_of
else -> R.string.unfollow_hashtag_of else -> R.string.unfollow_hashtag_of
} }
d.addAction(getString(toggleCaption, tagWithSharp)) { action(getString(toggleCaption, tagWithSharp)) {
followHashTag(accessInfo, tagWithoutSharp, toggle) followHashTag(accessInfo, tagWithoutSharp, toggle)
} }
} }
d.show(activity, tagWithSharp)
} catch (ex: Throwable) {
log.e(ex, "tagDialog failed.")
} }
} }
} }
@ -175,80 +171,80 @@ fun ActMain.tagTimelineFromAccount(
// 「投稿者別タグTL」を開くなら、投稿者のacctを指定する // 「投稿者別タグTL」を開くなら、投稿者のacctを指定する
acct: Acct? = null, acct: Acct? = null,
) { ) {
val activity = this
launchAndShowError {
actionsDialog("#$tagWithoutSharp") {
val dialog = ActionsDialog() val accountList = daoSavedAccount.loadAccountList().sortedByNickname()
val accountList = SavedAccount.loadAccountList(this) // 分類する
SavedAccount.sort(accountList) val listOriginal = ArrayList<SavedAccount>()
val listOriginalPseudo = ArrayList<SavedAccount>()
val listOther = ArrayList<SavedAccount>()
for (a in accountList) {
if (acct == null) {
when {
!a.matchHost(host) -> listOther.add(a)
a.isPseudo -> listOriginalPseudo.add(a)
else -> listOriginal.add(a)
}
} else {
when {
// 疑似アカウントはacctからaccount idを取得できないので
// アカウント別タグTLを開けない
a.isPseudo -> Unit
// 分類する // ミスキーはアカウント別タグTLがないので
val listOriginal = ArrayList<SavedAccount>() // アカウント別タグTLを開けない
val listOriginalPseudo = ArrayList<SavedAccount>() a.isMisskey -> Unit
val listOther = ArrayList<SavedAccount>()
for (a in accountList) { !a.matchHost(host) -> listOther.add(a)
if (acct == null) { else -> listOriginal.add(a)
when { }
!a.matchHost(host) -> listOther.add(a) }
a.isPseudo -> listOriginalPseudo.add(a)
else -> listOriginal.add(a)
} }
} else {
when {
// 疑似アカウントはacctからaccount idを取得できないので
// アカウント別タグTLを開けない
a.isPseudo -> Unit
// ミスキーはアカウント別タグTLがないので // ブラウザで表示する
// アカウント別タグTLを開けない if (!url.isNullOrBlank()) {
a.isMisskey -> Unit action(getString(R.string.open_web_on_host, host)) {
openCustomTab(url)
}
}
!a.matchHost(host) -> listOther.add(a) // 同タンスのアカウントがない場合は疑似アカウントを作成して開く
else -> listOriginal.add(a) // ただし疑似アカウントではアカウントの同期ができないため、特定ユーザのタグTLは読めない)
if (acct == null && listOriginal.isEmpty() && listOriginalPseudo.isEmpty()) {
action(getString(R.string.open_in_pseudo_account, "?@$host")) {
launchMain {
addPseudoAccount(host)?.let { tagTimeline(pos, it, tagWithoutSharp) }
}
}
}
// 分類した順に選択肢を追加する
for (a in listOriginal) {
action(
daoAcctColor.getStringWithNickname(activity, R.string.open_in_account, a.acct)
) {
tagTimeline(pos, a, tagWithoutSharp, acct?.ascii)
}
}
for (a in listOriginalPseudo) {
action(
daoAcctColor.getStringWithNickname(activity, R.string.open_in_account, a.acct)
) {
tagTimeline(pos, a, tagWithoutSharp, acct?.ascii)
}
}
for (a in listOther) {
action(
daoAcctColor.getStringWithNickname(activity, R.string.open_in_account, a.acct)
) {
tagTimeline(pos, a, tagWithoutSharp, acct?.ascii)
}
} }
} }
} }
// ブラウザで表示する
if (!url.isNullOrBlank()) {
dialog.addAction(getString(R.string.open_web_on_host, host)) {
openCustomTab(url)
}
}
// 同タンスのアカウントがない場合は疑似アカウントを作成して開く
// ただし疑似アカウントではアカウントの同期ができないため、特定ユーザのタグTLは読めない)
if (acct == null && listOriginal.isEmpty() && listOriginalPseudo.isEmpty()) {
dialog.addAction(getString(R.string.open_in_pseudo_account, "?@$host")) {
launchMain {
addPseudoAccount(host)?.let { tagTimeline(pos, it, tagWithoutSharp) }
}
}
}
// 分類した順に選択肢を追加する
for (a in listOriginal) {
dialog.addAction(
AcctColor.getStringWithNickname(
this,
R.string.open_in_account,
a.acct
)
) {
tagTimeline(pos, a, tagWithoutSharp, acct?.ascii)
}
}
for (a in listOriginalPseudo) {
dialog.addAction(AcctColor.getStringWithNickname(this, R.string.open_in_account, a.acct)) {
tagTimeline(pos, a, tagWithoutSharp, acct?.ascii)
}
}
for (a in listOther) {
dialog.addAction(AcctColor.getStringWithNickname(this, R.string.open_in_account, a.acct)) {
tagTimeline(pos, a, tagWithoutSharp, acct?.ascii)
}
}
dialog.show(this, "#$tagWithoutSharp")
} }
fun ActMain.followHashTag( fun ActMain.followHashTag(

View File

@ -12,6 +12,9 @@ import jp.juggler.subwaytooter.api.syncStatus
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.table.sortInplaceByNickname
import jp.juggler.subwaytooter.table.sortedByNickname
import jp.juggler.subwaytooter.util.matchHost import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
@ -102,7 +105,7 @@ fun ActMain.timelineLocal(
launchMain { launchMain {
// 指定タンスのアカウントを持ってるか? // 指定タンスのアカウントを持ってるか?
val accountList = ArrayList<SavedAccount>() val accountList = ArrayList<SavedAccount>()
for (a in SavedAccount.loadAccountList(applicationContext)) { for (a in daoSavedAccount.loadAccountList()) {
if (a.matchHost(host)) accountList.add(a) if (a.matchHost(host)) accountList.add(a)
} }
@ -113,12 +116,11 @@ fun ActMain.timelineLocal(
} }
} else { } else {
// 持ってるならアカウントを選んで開く // 持ってるならアカウントを選んで開く
SavedAccount.sort(accountList)
pickAccount( pickAccount(
bAllowPseudo = true, bAllowPseudo = true,
bAuto = false, bAuto = false,
message = getString(R.string.account_picker_add_timeline_of, host), message = getString(R.string.account_picker_add_timeline_of, host),
accountListArg = accountList accountListArg = accountList.sortedByNickname()
)?.let { addColumn(pos, it, ColumnType.LOCAL) } )?.let { addColumn(pos, it, ColumnType.LOCAL) }
} }
} }
@ -168,7 +170,7 @@ fun ActMain.timelineAroundByStatusAnotherAccount(
// 利用可能なアカウントを列挙する // 利用可能なアカウントを列挙する
val accountList1 = ArrayList<SavedAccount>() // 閲覧アカウントとホストが同じ val accountList1 = ArrayList<SavedAccount>() // 閲覧アカウントとホストが同じ
val accountList2 = ArrayList<SavedAccount>() // その他実アカウント val accountList2 = ArrayList<SavedAccount>() // その他実アカウント
label@ for (a in SavedAccount.loadAccountList(this)) { label@ for (a in daoSavedAccount.loadAccountList()) {
// Misskeyアカウントはステータスの同期が出来ないので選択させない // Misskeyアカウントはステータスの同期が出来ないので選択させない
if (a.isNA || a.isMisskey) continue if (a.isNA || a.isMisskey) continue
when { when {
@ -179,8 +181,8 @@ fun ActMain.timelineAroundByStatusAnotherAccount(
!a.isPseudo -> accountList2.add(a) !a.isPseudo -> accountList2.add(a)
} }
} }
SavedAccount.sort(accountList1) accountList1.sortInplaceByNickname()
SavedAccount.sort(accountList2) accountList2.sortInplaceByNickname()
accountList1.addAll(accountList2) accountList1.addAll(accountList2)
if (accountList1.isEmpty()) { if (accountList1.isEmpty()) {

View File

@ -14,10 +14,7 @@ import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.ReportForm import jp.juggler.subwaytooter.dialog.ReportForm
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.table.FavMute
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.util.matchHost import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.subwaytooter.util.openCustomTab import jp.juggler.subwaytooter.util.openCustomTab
import jp.juggler.util.* import jp.juggler.util.*
@ -72,9 +69,9 @@ fun ActMain.openAvatarImage(who: TootAccount) {
fun ActMain.clickHideFavourite( fun ActMain.clickHideFavourite(
accessInfo: SavedAccount, accessInfo: SavedAccount,
who: TootAccount, who: TootAccount,
) { ) = launchAndShowError {
val acct = accessInfo.getFullAcct(who) val acct = accessInfo.getFullAcct(who)
FavMute.save(acct) daoFavMute.save(acct)
showToast(false, R.string.changed) showToast(false, R.string.changed)
for (column in appState.columnList) { for (column in appState.columnList) {
column.onHideFavouriteNotification(acct) column.onHideFavouriteNotification(acct)
@ -84,8 +81,8 @@ fun ActMain.clickHideFavourite(
fun ActMain.clickShowFavourite( fun ActMain.clickShowFavourite(
accessInfo: SavedAccount, accessInfo: SavedAccount,
who: TootAccount, who: TootAccount,
) { ) = launchAndShowError {
FavMute.delete(accessInfo.getFullAcct(who)) daoFavMute.delete(accessInfo.getFullAcct(who))
showToast(false, R.string.changed) showToast(false, R.string.changed)
} }
@ -110,133 +107,134 @@ private fun ActMain.userMute(
bMute: Boolean, bMute: Boolean,
bMuteNotification: Boolean, bMuteNotification: Boolean,
duration: Int?, duration: Int?,
) { ) = launchAndShowError {
val whoAcct = whoAccessInfo.getFullAcct(whoArg) val whoAcct = whoAccessInfo.getFullAcct(whoArg)
if (accessInfo.isMe(whoAcct)) { if (accessInfo.isMe(whoAcct)) {
showToast(false, R.string.it_is_you) showToast(false, R.string.it_is_you)
return return@launchAndShowError
} }
launchMain { var resultRelation: UserRelation? = null
var resultRelation: UserRelation? = null var resultWhoId: EntityId? = null
var resultWhoId: EntityId? = null runApiTask(accessInfo) { client ->
runApiTask(accessInfo) { client -> val parser = TootParser(this, accessInfo)
val parser = TootParser(this, accessInfo) if (accessInfo.isPseudo) {
if (accessInfo.isPseudo) { if (!whoAcct.isValidFull) {
if (!whoAcct.isValidFull) { TootApiResult("can't mute pseudo acct ${whoAcct.pretty}")
TootApiResult("can't mute pseudo acct ${whoAcct.pretty}") } else {
} else { val relation = daoUserRelation.loadPseudo(whoAcct)
val relation = UserRelation.loadPseudo(whoAcct) relation.muting = bMute
relation.muting = bMute daoUserRelation.savePseudo(whoAcct.ascii, relation)
relation.savePseudo(whoAcct.ascii) resultRelation = relation
resultRelation = relation resultWhoId = whoArg.id
resultWhoId = whoArg.id TootApiResult()
TootApiResult() }
} else {
val whoId = if (accessInfo.matchHost(whoAccessInfo)) {
whoArg.id
} else {
val (result, accountRef) = client.syncAccountByAcct(accessInfo, whoAcct)
accountRef?.get()?.id ?: return@runApiTask result
}
resultWhoId = whoId
if (accessInfo.isMisskey) {
client.request(
when (bMute) {
true -> "/api/mute/create"
else -> "/api/mute/delete"
},
accessInfo.putMisskeyApiToken().apply {
put("userId", whoId.toString())
}.toPostRequestBuilder()
)?.apply {
if (jsonObject != null) {
// 204 no content
// update user relation
val ur = daoUserRelation.load(accessInfo.db_id, whoId)
ur.muting = bMute
daoUserRelation.saveUserRelationMisskey(
accessInfo,
whoId,
parser
)
resultRelation = ur
}
} }
} else { } else {
val whoId = if (accessInfo.matchHost(whoAccessInfo)) { client.request(
whoArg.id "/api/v1/accounts/$whoId/${if (bMute) "mute" else "unmute"}",
} else { when {
val (result, accountRef) = client.syncAccountByAcct(accessInfo, whoAcct) !bMute -> "".toFormRequestBody()
accountRef?.get()?.id ?: return@runApiTask result else ->
} buildJsonObject {
resultWhoId = whoId put("notifications", bMuteNotification)
if (duration != null) put("duration", duration)
if (accessInfo.isMisskey) { }.toRequestBody()
client.request( }.toPost()
when (bMute) { )?.apply {
true -> "/api/mute/create" val jsonObject = jsonObject
else -> "/api/mute/delete" if (jsonObject != null) {
}, resultRelation = daoUserRelation.saveUserRelation(
accessInfo.putMisskeyApiToken().apply { accessInfo,
put("userId", whoId.toString()) parseItem(::TootRelationShip, parser, jsonObject)
}.toPostRequestBuilder() )
)?.apply {
if (jsonObject != null) {
// 204 no content
// update user relation
val ur = UserRelation.load(accessInfo.db_id, whoId)
ur.muting = bMute
accessInfo.saveUserRelationMisskey(
whoId,
parser
)
resultRelation = ur
}
}
} else {
client.request(
"/api/v1/accounts/$whoId/${if (bMute) "mute" else "unmute"}",
when {
!bMute -> "".toFormRequestBody()
else ->
buildJsonObject {
put("notifications", bMuteNotification)
if (duration != null) put("duration", duration)
}.toRequestBody()
}.toPost()
)?.apply {
val jsonObject = jsonObject
if (jsonObject != null) {
resultRelation = accessInfo.saveUserRelation(
parseItem(::TootRelationShip, parser, jsonObject)
)
}
} }
} }
} }
}?.let { result -> }
val relation = resultRelation }?.let { result ->
val whoId = resultWhoId val relation = resultRelation
if (relation == null || whoId == null) { val whoId = resultWhoId
showToast(false, result.error) if (relation == null || whoId == null) {
} else { showToast(false, result.error)
// 未確認だが、自分をミュートしようとするとリクエストは成功するがレスポンス中のmutingはfalseになるはず } else {
if (bMute && !relation.muting) { // 未確認だが、自分をミュートしようとするとリクエストは成功するがレスポンス中のmutingはfalseになるはず
showToast(false, R.string.not_muted) if (bMute && !relation.muting) {
return@launchMain showToast(false, R.string.not_muted)
} return@launchAndShowError
}
for (column in appState.columnList) { for (column in appState.columnList) {
if (column.accessInfo.isPseudo) { if (column.accessInfo.isPseudo) {
if (relation.muting && column.type != ColumnType.PROFILE) { if (relation.muting && column.type != ColumnType.PROFILE) {
// ミュートしたユーザの情報はTLから消える // ミュートしたユーザの情報はTLから消える
column.removeAccountInTimelinePseudo(whoAcct) column.removeAccountInTimelinePseudo(whoAcct)
} }
// フォローアイコンの表示更新が走る // フォローアイコンの表示更新が走る
column.updateFollowIcons(accessInfo) column.updateFollowIcons(accessInfo)
} else if (column.accessInfo == accessInfo) { } else if (column.accessInfo == accessInfo) {
when { when {
!relation.muting -> { !relation.muting -> {
if (column.type == ColumnType.MUTES) { if (column.type == ColumnType.MUTES) {
// ミュート解除したら「ミュートしたユーザ」カラムから消える // ミュート解除したら「ミュートしたユーザ」カラムから消える
column.removeUser(accessInfo, ColumnType.MUTES, whoId) column.removeUser(accessInfo, ColumnType.MUTES, whoId)
} else { } else {
// 他のカラムではフォローアイコンの表示更新が走る // 他のカラムではフォローアイコンの表示更新が走る
column.updateFollowIcons(accessInfo)
}
}
column.type == ColumnType.PROFILE && column.profileId == whoId -> {
// 該当ユーザのプロフページのトゥートはミュートしてても見れる
// しかしフォローアイコンの表示更新は必要
column.updateFollowIcons(accessInfo) column.updateFollowIcons(accessInfo)
} }
}
else -> { column.type == ColumnType.PROFILE && column.profileId == whoId -> {
// ミュートしたユーザの情報はTLから消える // 該当ユーザのプロフページのトゥートはミュートしてても見れる
column.removeAccountInTimeline(accessInfo, whoId) // しかしフォローアイコンの表示更新は必要
} column.updateFollowIcons(accessInfo)
}
else -> {
// ミュートしたユーザの情報はTLから消える
column.removeAccountInTimeline(accessInfo, whoId)
} }
} }
} }
showToast(
false,
if (relation.muting) R.string.mute_succeeded else R.string.unmute_succeeded
)
} }
showToast(
false,
if (relation.muting) R.string.mute_succeeded else R.string.unmute_succeeded
)
} }
} }
} }
@ -258,7 +256,7 @@ fun ActMain.userMuteConfirm(
accessInfo: SavedAccount, accessInfo: SavedAccount,
who: TootAccount, who: TootAccount,
whoAccessInfo: SavedAccount, whoAccessInfo: SavedAccount,
) { ) = launchAndShowError {
val activity = this@userMuteConfirm val activity = this@userMuteConfirm
// Mastodon 3.3から時限ミュート設定ができる // Mastodon 3.3から時限ミュート設定ができる
@ -288,59 +286,56 @@ fun ActMain.userMuteConfirm(
cbMuteNotification.vg(hasMuteNotification) cbMuteNotification.vg(hasMuteNotification)
?.setText(R.string.confirm_mute_notification_for_user) ?.setText(R.string.confirm_mute_notification_for_user)
launchMain { val spMuteDuration: Spinner = view.findViewById(R.id.spMuteDuration)
val spMuteDuration: Spinner = view.findViewById(R.id.spMuteDuration) val hasMuteDuration = try {
val hasMuteDuration = try { when {
when { accessInfo.isMisskey || accessInfo.isPseudo -> false
accessInfo.isMisskey || accessInfo.isPseudo -> false else -> {
else -> { var resultBoolean = false
var resultBoolean = false runApiTask(accessInfo) { client ->
runApiTask(accessInfo) { client -> val (ti, ri) = TootInstance.get(client)
val (ti, ri) = TootInstance.get(client) resultBoolean = ti?.versionGE(TootInstance.VERSION_3_3_0_rc1) == true
resultBoolean = ti?.versionGE(TootInstance.VERSION_3_3_0_rc1) == true ri
ri
}
resultBoolean
}
}
} catch (ignored: CancellationException) {
// not show error
return@launchMain
} catch (ex: RuntimeException) {
showToast(true, ex.message)
return@launchMain
}
if (hasMuteDuration) {
view.findViewById<View>(R.id.llMuteDuration).vg(true)
spMuteDuration.apply {
adapter = ArrayAdapter(
activity,
android.R.layout.simple_spinner_item,
choiceList.map { it.second }.toTypedArray(),
).apply {
setDropDownViewResource(R.layout.lv_spinner_dropdown)
} }
resultBoolean
} }
} }
} catch (ignored: CancellationException) {
AlertDialog.Builder(activity) // not show error
.setView(view) return@launchAndShowError
.setNegativeButton(R.string.cancel, null) } catch (ex: RuntimeException) {
.setPositiveButton(R.string.ok) { _, _ -> showToast(true, ex.message)
userMute( return@launchAndShowError
accessInfo,
who,
whoAccessInfo,
bMute = true,
bMuteNotification = cbMuteNotification.isChecked,
duration = spMuteDuration.selectedItemPosition
.takeIf { hasMuteDuration && it in choiceList.indices }
?.let { choiceList[it].first }
)
}
.show()
} }
if (hasMuteDuration) {
view.findViewById<View>(R.id.llMuteDuration).vg(true)
spMuteDuration.apply {
adapter = ArrayAdapter(
activity,
android.R.layout.simple_spinner_item,
choiceList.map { it.second }.toTypedArray(),
).apply {
setDropDownViewResource(R.layout.lv_spinner_dropdown)
}
}
}
AlertDialog.Builder(activity)
.setView(view)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
userMute(
accessInfo,
who,
whoAccessInfo,
bMute = true,
bMuteNotification = cbMuteNotification.isChecked,
duration = spMuteDuration.selectedItemPosition
.takeIf { hasMuteDuration && it in choiceList.indices }
?.let { choiceList[it].first }
)
}.show()
} }
fun ActMain.userMuteFromAnotherAccount( fun ActMain.userMuteFromAnotherAccount(
@ -381,9 +376,9 @@ fun ActMain.userBlock(
if (whoAcct.ascii.contains('?')) { if (whoAcct.ascii.contains('?')) {
TootApiResult("can't block pseudo account ${whoAcct.pretty}") TootApiResult("can't block pseudo account ${whoAcct.pretty}")
} else { } else {
val relation = UserRelation.loadPseudo(whoAcct) val relation = daoUserRelation.loadPseudo(whoAcct)
relation.blocking = bBlock relation.blocking = bBlock
relation.savePseudo(whoAcct.ascii) daoUserRelation.savePseudo(whoAcct.ascii, relation)
relationResult = relation relationResult = relation
TootApiResult() TootApiResult()
} }
@ -397,11 +392,10 @@ fun ActMain.userBlock(
whoIdResult = whoId whoIdResult = whoId
if (accessInfo.isMisskey) { if (accessInfo.isMisskey) {
fun saveBlock(v: Boolean) { fun saveBlock(v: Boolean) {
val ur = UserRelation.load(accessInfo.db_id, whoId) val ur = daoUserRelation.load(accessInfo.db_id, whoId)
ur.blocking = v ur.blocking = v
UserRelation.save1Misskey( daoUserRelation.save1Misskey(
System.currentTimeMillis(), System.currentTimeMillis(),
accessInfo.db_id, accessInfo.db_id,
whoId.toString(), whoId.toString(),
@ -434,7 +428,8 @@ fun ActMain.userBlock(
"".toFormRequestBody().toPost() "".toFormRequestBody().toPost()
)?.also { result -> )?.also { result ->
val parser = TootParser(this, accessInfo) val parser = TootParser(this, accessInfo)
relationResult = accessInfo.saveUserRelation( relationResult = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject) parseItem(::TootRelationShip, parser, result.jsonObject)
) )
} }
@ -597,7 +592,7 @@ fun ActMain.userProfileFromAnotherAccount(
bAuto = false, bAuto = false,
message = getString( message = getString(
R.string.account_picker_open_user_who, R.string.account_picker_open_user_who,
AcctColor.getNickname(accessInfo, who) daoAcctColor.getNickname(accessInfo, who)
), ),
accountListArg = accountListNonPseudo(who.apDomain) accountListArg = accountListNonPseudo(who.apDomain)
)?.let { ai -> )?.let { ai ->
@ -630,7 +625,7 @@ fun ActMain.userProfile(
acct: Acct, acct: Acct,
userUrl: String, userUrl: String,
originalUrl: String = userUrl, originalUrl: String = userUrl,
) { ) = launchAndShowError {
if (accessInfo?.isPseudo == false) { if (accessInfo?.isPseudo == false) {
// 文脈のアカウントがあり、疑似アカウントではない // 文脈のアカウントがあり、疑似アカウントではない
@ -655,51 +650,49 @@ fun ActMain.userProfile(
} }
} }
} }
return return@launchAndShowError
} }
// 文脈がない、もしくは疑似アカウントだった // 文脈がない、もしくは疑似アカウントだった
// 疑似アカウントでは検索APIを使えないため、IDが分からない // 疑似アカウントでは検索APIを使えないため、IDが分からない
if (!SavedAccount.hasRealAccount()) { if (!daoSavedAccount.hasRealAccount()) {
// 疑似アカウントしか登録されていない // 疑似アカウントしか登録されていない
// chrome tab で開く // chrome tab で開く
openCustomTab(originalUrl) openCustomTab(originalUrl)
return return@launchAndShowError
} }
launchMain { val activity = this@userProfile
val activity = this@userProfile pickAccount(
pickAccount( bAllowPseudo = false,
bAllowPseudo = false, bAuto = false,
bAuto = false, message = getString(
message = getString( R.string.account_picker_open_user_who,
R.string.account_picker_open_user_who, daoAcctColor.getNickname(acct)
AcctColor.getNickname(acct) ),
), accountListArg = accountListNonPseudo(acct.host),
accountListArg = accountListNonPseudo(acct.host), extraCallback = { ll, pad_se, pad_tb ->
extraCallback = { ll, pad_se, pad_tb -> // chrome tab で開くアクションを追加
// chrome tab で開くアクションを追加 val lp = LinearLayout.LayoutParams(
val lp = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT
LinearLayout.LayoutParams.WRAP_CONTENT )
) val b = AppCompatButton(activity)
val b = AppCompatButton(activity) b.setPaddingRelative(pad_se, pad_tb, pad_se, pad_tb)
b.setPaddingRelative(pad_se, pad_tb, pad_se, pad_tb) b.gravity = Gravity.START or Gravity.CENTER_VERTICAL
b.gravity = Gravity.START or Gravity.CENTER_VERTICAL b.isAllCaps = false
b.isAllCaps = false b.layoutParams = lp
b.layoutParams = lp b.minHeight = (0.5f + 32f * activity.density).toInt()
b.minHeight = (0.5f + 32f * activity.density).toInt() b.text = getString(R.string.open_in_browser)
b.text = getString(R.string.open_in_browser) b.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
b.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
b.setOnClickListener { b.setOnClickListener {
openCustomTab(originalUrl) openCustomTab(originalUrl)
}
ll.addView(b, 0)
} }
)?.let { ll.addView(b, 0)
userProfileFromUrlOrAcct(pos, it, acct, userUrl)
} }
)?.let {
userProfileFromUrlOrAcct(pos, it, acct, userUrl)
} }
} }
@ -798,12 +791,9 @@ fun ActMain.userSetShowBoosts(
jsonObjectOf("reblogs" to bShow).toPostRequestBuilder() jsonObjectOf("reblogs" to bShow).toPostRequestBuilder()
)?.also { result -> )?.also { result ->
val parser = TootParser(this, accessInfo) val parser = TootParser(this, accessInfo)
resultRelation = accessInfo.saveUserRelation( resultRelation = daoUserRelation.saveUserRelation(
parseItem( accessInfo,
::TootRelationShip, parseItem(::TootRelationShip, parser, result.jsonObject)
parser,
result.jsonObject
)
) )
} }
}?.let { result -> }?.let { result ->
@ -860,7 +850,7 @@ fun ActMain.userSetStatusNotification(
result.jsonObject result.jsonObject
) )
if (relation != null) { if (relation != null) {
UserRelation.save1Mastodon( daoUserRelation.save1Mastodon(
System.currentTimeMillis(), System.currentTimeMillis(),
accessInfo.db_id, accessInfo.db_id,
relation relation
@ -901,7 +891,8 @@ fun ActMain.userEndorsement(
) )
?.also { result -> ?.also { result ->
val parser = TootParser(this, accessInfo) val parser = TootParser(this, accessInfo)
resultRelation = accessInfo.saveUserRelation( resultRelation = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject) parseItem(::TootRelationShip, parser, result.jsonObject)
) )
} }

View File

@ -4,6 +4,7 @@ import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.column.fireShowColumnHeader import jp.juggler.subwaytooter.column.fireShowColumnHeader
import jp.juggler.subwaytooter.pref.PrefL import jp.juggler.subwaytooter.pref.PrefL
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoSavedAccount
// デフォルトの投稿先アカウントを探す。アカウント選択が必要な状況ならnull // デフォルトの投稿先アカウントを探す。アカウント選択が必要な状況ならnull
val ActMain.currentPostTarget: SavedAccount? val ActMain.currentPostTarget: SavedAccount?
@ -17,9 +18,9 @@ val ActMain.currentPostTarget: SavedAccount?
}, },
{ env -> { env ->
val dbId = PrefL.lpTabletTootDefaultAccount() val dbId = PrefL.lpTabletTootDefaultAccount.value
if (dbId != -1L) { if (dbId != -1L) {
val a = SavedAccount.loadAccount(this@currentPostTarget, dbId) val a = daoSavedAccount.loadAccount(dbId)
if (a != null && !a.isPseudo) return a if (a != null && !a.isPseudo) return a
} }
@ -47,24 +48,23 @@ val ActMain.currentPostTarget: SavedAccount?
}) })
fun ActMain.reloadAccountSetting( fun ActMain.reloadAccountSetting(
newAccounts: ArrayList<SavedAccount> = SavedAccount.loadAccountList( newAccounts: List<SavedAccount>,
this
),
) { ) {
for (column in appState.columnList) { for (column in appState.columnList) {
val a = column.accessInfo val a = column.accessInfo
if (!a.isNA) a.reloadSetting(this, newAccounts.find { it.acct == a.acct }) val b = newAccounts.find { it.acct == a.acct }
if (!a.isNA && b != null) daoSavedAccount.reloadSetting(a, b)
column.fireShowColumnHeader() column.fireShowColumnHeader()
} }
} }
fun ActMain.reloadAccountSetting(account: SavedAccount) { fun ActMain.reloadAccountSetting(account: SavedAccount) {
val newData = SavedAccount.loadAccount(this, account.db_id) val newData = daoSavedAccount.loadAccount(account.db_id)
?: return ?: return
for (column in appState.columnList) { for (column in appState.columnList) {
val a = column.accessInfo val a = column.accessInfo
if (a.acct != newData.acct) continue if (a.acct != newData.acct) continue
if (!a.isNA) a.reloadSetting(this, newData) if (!a.isNA) daoSavedAccount.reloadSetting(a, newData)
column.fireShowColumnHeader() column.fireShowColumnHeader()
} }
} }

View File

@ -5,6 +5,7 @@ import android.text.Spannable
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.work.WorkManager
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.action.openColumnList import jp.juggler.subwaytooter.action.openColumnList
@ -17,86 +18,93 @@ import jp.juggler.subwaytooter.columnviewholder.ColumnViewHolder
import jp.juggler.subwaytooter.columnviewholder.TabletColumnViewHolder import jp.juggler.subwaytooter.columnviewholder.TabletColumnViewHolder
import jp.juggler.subwaytooter.columnviewholder.ViewHolderHeaderBase import jp.juggler.subwaytooter.columnviewholder.ViewHolderHeaderBase
import jp.juggler.subwaytooter.columnviewholder.ViewHolderItem import jp.juggler.subwaytooter.columnviewholder.ViewHolderItem
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.itemviewholder.ItemViewHolder import jp.juggler.subwaytooter.itemviewholder.ItemViewHolder
import jp.juggler.subwaytooter.pref.* import jp.juggler.subwaytooter.pref.*
import jp.juggler.subwaytooter.push.PushWorker
import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.util.checkPrivacyPolicy
import jp.juggler.subwaytooter.util.openCustomTab import jp.juggler.subwaytooter.util.openCustomTab
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.addTo import jp.juggler.util.data.addTo
import jp.juggler.util.data.cast import jp.juggler.util.data.cast
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.ui.dismissSafe
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
private val log = LogCategory("ActMainActions") private val log = LogCategory("ActMainActions")
fun ActMain.onBackPressedImpl() { fun ActMain.onBackPressedImpl() {
launchAndShowError {
// メニューが開いていたら閉じる // メニューが開いていたら閉じる
if (drawer.isDrawerOpen(GravityCompat.START)) { if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START) drawer.closeDrawer(GravityCompat.START)
return return@launchAndShowError
}
// カラムが0個ならアプリを終了する
if (appState.columnCount == 0) {
finish()
return
}
// カラム設定が開いているならカラム設定を閉じる
if (closeColumnSetting()) {
return
}
fun getClosableColumnList(): List<Column> {
val visibleColumnList = ArrayList<Column>()
phoneTab({ env ->
try {
appState.column(env.pager.currentItem)?.addTo(visibleColumnList)
} catch (ex: Throwable) {
log.e(ex, "getClosableColumnList failed.")
}
}, { env ->
visibleColumnList.addAll(env.visibleColumns)
})
return visibleColumnList.filter { !it.dontClose }
}
// カラムが1個以上ある場合は設定に合わせて挙動を変える
when (PrefI.ipBackButtonAction.invoke(pref)) {
PrefI.BACK_EXIT_APP -> finish()
PrefI.BACK_OPEN_COLUMN_LIST -> openColumnList()
PrefI.BACK_CLOSE_COLUMN -> {
val closeableColumnList = getClosableColumnList()
when (closeableColumnList.size) {
0 -> when {
PrefB.bpExitAppWhenCloseProtectedColumn(pref) &&
PrefB.bpDontConfirmBeforeCloseColumn.invoke(pref) ->
finish()
else -> showToast(false, R.string.missing_closeable_column)
}
1 -> closeColumn(closeableColumnList.first())
else -> showToast(
false,
R.string.cant_close_column_by_back_button_when_multiple_column_shown
)
}
} }
else /* PrefI.BACK_ASK_ALWAYS */ -> {
val closeableColumnList = getClosableColumnList() // カラムが0個ならアプリを終了する
val dialog = ActionsDialog() if (appState.columnCount == 0) {
if (closeableColumnList.size == 1) { finish()
val column = closeableColumnList.first() return@launchAndShowError
dialog.addAction(getString(R.string.close_column)) { }
closeColumn(column, bConfirmed = true)
// カラム設定が開いているならカラム設定を閉じる
if (closeColumnSetting()) {
return@launchAndShowError
}
fun getClosableColumnList(): List<Column> {
val visibleColumnList = ArrayList<Column>()
phoneTab({ env ->
try {
appState.column(env.pager.currentItem)?.addTo(visibleColumnList)
} catch (ex: Throwable) {
log.e(ex, "getClosableColumnList failed.")
}
}, { env ->
visibleColumnList.addAll(env.visibleColumns)
})
return visibleColumnList.filter { !it.dontClose }
}
// カラムが1個以上ある場合は設定に合わせて挙動を変える
when (PrefI.ipBackButtonAction.value) {
PrefI.BACK_EXIT_APP -> finish()
PrefI.BACK_OPEN_COLUMN_LIST -> openColumnList()
PrefI.BACK_CLOSE_COLUMN -> {
val closeableColumnList = getClosableColumnList()
when (closeableColumnList.size) {
0 -> when {
PrefB.bpExitAppWhenCloseProtectedColumn.value &&
PrefB.bpDontConfirmBeforeCloseColumn.value ->
finish()
else -> showToast(false, R.string.missing_closeable_column)
}
1 -> closeColumn(closeableColumnList.first())
else -> showToast(
false,
R.string.cant_close_column_by_back_button_when_multiple_column_shown
)
} }
} }
dialog.addAction(getString(R.string.open_column_list)) { openColumnList() } /* PrefI.BACK_ASK_ALWAYS */
dialog.addAction(getString(R.string.app_exit)) { finish() } else -> actionsDialog {
dialog.show(this, null) val closeableColumnList = getClosableColumnList()
if (closeableColumnList.size == 1) {
val column = closeableColumnList.first()
action(getString(R.string.close_column)) {
closeColumn(column, bConfirmed = true)
}
}
action(getString(R.string.open_column_list)) { openColumnList() }
action(getString(R.string.app_exit)) { finish() }
}
} }
} }
} }
@ -178,23 +186,23 @@ fun ActMain.onMyClickableSpanClickedImpl(viewClicked: View, span: MyClickableSpa
) )
} }
fun ActMain.themeDefaultChangedDialog() { suspend fun ActMain.themeDefaultChangedDialog() {
val lpThemeDefaultChangedWarnTime = PrefL.lpThemeDefaultChangedWarnTime val lpThemeDefaultChangedWarnTime = PrefL.lpThemeDefaultChangedWarnTime
val ipUiTheme = PrefI.ipUiTheme val ipUiTheme = PrefI.ipUiTheme
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
// テーマが未定義でなければ警告しない // テーマが未定義でなければ警告しない
if (pref.getInt(ipUiTheme.key, -1) != -1) { if (lazyPref.getInt(ipUiTheme.key, -1) != -1) {
log.i("themeDefaultChangedDialog: theme was set.") log.i("themeDefaultChangedDialog: theme was set.")
return return
} }
// 頻繁には警告しない // 頻繁には警告しない
if (now - lpThemeDefaultChangedWarnTime.invoke(pref) < TimeUnit.DAYS.toMillis(60L)) { if (now - lpThemeDefaultChangedWarnTime.value < TimeUnit.DAYS.toMillis(60L)) {
log.i("themeDefaultChangedDialog: avoid frequently check.") log.i("themeDefaultChangedDialog: avoid frequently check.")
return return
} }
pref.edit().put(lpThemeDefaultChangedWarnTime, now).apply() lpThemeDefaultChangedWarnTime.value = now
// 色がすべてデフォルトなら警告不要 // 色がすべてデフォルトなら警告不要
val customizedKeys = ArrayList<String>() val customizedKeys = ArrayList<String>()
@ -202,18 +210,50 @@ fun ActMain.themeDefaultChangedDialog() {
item.pref?.let { p -> item.pref?.let { p ->
when { when {
p == PrefS.spBoostAlpha -> Unit p == PrefS.spBoostAlpha -> Unit
p.hasNonDefaultValue(pref) -> customizedKeys.add(p.key) p.hasNonDefaultValue() -> customizedKeys.add(p.key)
} }
} }
} }
log.w("themeDefaultChangedDialog: customizedKeys=${customizedKeys.joinToString(",")}")
if (customizedKeys.isEmpty()) { if (customizedKeys.isEmpty()) {
pref.edit().put(ipUiTheme, ipUiTheme.defVal).apply() ipUiTheme.value = ipUiTheme.defVal
return return
} }
log.w("themeDefaultChangedDialog: customizedKeys=${customizedKeys.joinToString(",")}")
AlertDialog.Builder(this) suspendCancellableCoroutine { cont ->
.setMessage(R.string.color_theme_changed) val dialog = AlertDialog.Builder(this)
.setPositiveButton(android.R.string.ok, null) .setMessage(R.string.color_theme_changed)
.show() .setPositiveButton(android.R.string.ok, null)
.setOnDismissListener {
if (cont.isActive) cont.resume(Unit) {}
}
.create()
cont.invokeOnCancellation { dialog.dismissSafe() }
dialog.show()
}
}
fun ActMain.launchDialogs() {
launchAndShowError {
// プライバシーポリシー
val agreed = try {
checkPrivacyPolicy()
} catch (ex: Throwable) {
log.e(ex, "checkPrivacyPolicy failed.")
return@launchAndShowError
}
// 同意がないなら残りの何かは表示しない
if (!agreed) return@launchAndShowError
// テーマ告知
themeDefaultChangedDialog()
// 通知権限の確認
if(!prNotification.checkOrLaunch()) return@launchAndShowError
// Workの掃除
WorkManager.getInstance(applicationContext).pruneWork()
// 定期的にendpointを再登録したい
PushWorker.enqueueRegisterEndpoint(applicationContext, keepAliveMode = true)
}
} }

View File

@ -65,7 +65,7 @@ fun ActMain.refreshAfterPost() {
this.postedRedraftId = null this.postedRedraftId = null
} }
val refreshAfterToot = PrefI.ipRefreshAfterToot(pref) val refreshAfterToot = PrefI.ipRefreshAfterToot.value
if (refreshAfterToot != PrefI.RAT_DONT_REFRESH) { if (refreshAfterToot != PrefI.RAT_DONT_REFRESH) {
appState.columnList appState.columnList
.filter { it.accessInfo.acct == postedAcct } .filter { it.accessInfo.acct == postedAcct }

View File

@ -12,7 +12,7 @@ import java.lang.ref.WeakReference
// AutoCWの基準幅を計算する // AutoCWの基準幅を計算する
fun ActMain.resizeAutoCW(columnW: Int) { fun ActMain.resizeAutoCW(columnW: Int) {
val sv = PrefS.spAutoCWLines(pref) val sv = PrefS.spAutoCWLines.value
nAutoCwLines = sv.toIntOrNull() ?: -1 nAutoCwLines = sv.toIntOrNull() ?: -1
if (nAutoCwLines > 0) { if (nAutoCwLines > 0) {
val lvPad = (0.5f + 12 * density).toInt() val lvPad = (0.5f + 12 * density).toInt()

View File

@ -16,8 +16,8 @@ import jp.juggler.subwaytooter.columnviewholder.showColumnSetting
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.PrefS import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.data.clip import jp.juggler.util.data.clip
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -103,7 +103,7 @@ fun ActMain.addColumn(
vararg params: Any, vararg params: Any,
): Column { ): Column {
return addColumn( return addColumn(
PrefB.bpAllowColumnDuplication(pref), PrefB.bpAllowColumnDuplication.value,
indexArg, indexArg,
ai, ai,
type, type,
@ -175,7 +175,7 @@ fun ActMain.updateColumnStrip() {
viewRoot.tag = index viewRoot.tag = index
viewRoot.setOnClickListener { v -> viewRoot.setOnClickListener { v ->
val idx = v.tag as Int val idx = v.tag as Int
if (PrefB.bpScrollTopFromColumnStrip(pref) && isVisibleColumn(idx)) { if (PrefB.bpScrollTopFromColumnStrip.value && isVisibleColumn(idx)) {
column.viewHolder?.scrollToTop2() column.viewHolder?.scrollToTop2()
return@setOnClickListener return@setOnClickListener
} }
@ -193,9 +193,9 @@ fun ActMain.updateColumnStrip() {
ivIcon.imageTintList = ColorStateList.valueOf(column.getHeaderNameColor()) ivIcon.imageTintList = ColorStateList.valueOf(column.getHeaderNameColor())
// //
val ac = AcctColor.load(column.accessInfo) val ac = daoAcctColor.load(column.accessInfo)
if (AcctColor.hasColorForeground(ac)) { if (daoAcctColor.hasColorForeground(ac)) {
vAcctColor.setBackgroundColor(ac.color_fg) vAcctColor.setBackgroundColor(ac.colorFg)
} else { } else {
vAcctColor.visibility = View.INVISIBLE vAcctColor.visibility = View.INVISIBLE
} }
@ -214,7 +214,7 @@ fun ActMain.closeColumn(column: Column, bConfirmed: Boolean = false) {
return return
} }
if (!bConfirmed && !PrefB.bpDontConfirmBeforeCloseColumn(pref)) { if (!bConfirmed && !PrefB.bpDontConfirmBeforeCloseColumn.value) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(R.string.confirm_close_column) .setMessage(R.string.confirm_close_column)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
@ -366,7 +366,7 @@ fun ActMain.scrollToColumn(index: Int, smoothScroll: Boolean = true) {
fun ActMain.scrollToLastColumn() { fun ActMain.scrollToLastColumn() {
if (appState.columnCount <= 0) return if (appState.columnCount <= 0) return
val columnPos = PrefI.ipLastColumnPos(pref) val columnPos = PrefI.ipLastColumnPos.value
log.d("ipLastColumnPos load $columnPos") log.d("ipLastColumnPos load $columnPos")
// 前回最後に表示していたカラムの位置にスクロールする // 前回最後に表示していたカラムの位置にスクロールする
@ -385,7 +385,7 @@ fun ActMain.scrollToLastColumn() {
fun ActMain.resizeColumnWidth(views: ActMainTabletViews) { fun ActMain.resizeColumnWidth(views: ActMainTabletViews) {
var columnWMinDp = ActMain.COLUMN_WIDTH_MIN_DP var columnWMinDp = ActMain.COLUMN_WIDTH_MIN_DP
val sv = PrefS.spColumnWidth(pref) val sv = PrefS.spColumnWidth.value
if (sv.isNotEmpty()) { if (sv.isNotEmpty()) {
try { try {
val iv = Integer.parseInt(sv) val iv = Integer.parseInt(sv)

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri import androidx.core.net.toUri
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
@ -18,20 +19,35 @@ import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.findStatusIdFromU
import jp.juggler.subwaytooter.api.entity.TootVisibility import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.api.runApiTask2 import jp.juggler.subwaytooter.api.runApiTask2
import jp.juggler.subwaytooter.api.showApiError import jp.juggler.subwaytooter.api.showApiError
import jp.juggler.subwaytooter.auth.authRepo
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.startLoading import jp.juggler.subwaytooter.column.startLoading
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.dialog.runInProgress
import jp.juggler.subwaytooter.notification.PushSubscriptionHelper import jp.juggler.subwaytooter.notification.PushSubscriptionHelper
import jp.juggler.subwaytooter.notification.checkNotificationImmediate import jp.juggler.subwaytooter.notification.checkNotificationImmediate
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
import jp.juggler.subwaytooter.notification.recycleClickedNotification import jp.juggler.subwaytooter.notification.recycleClickedNotification
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.PushWorker
import jp.juggler.subwaytooter.push.fcmHandler
import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.decodePercent import jp.juggler.util.data.decodePercent
import jp.juggler.util.data.groupEx import jp.juggler.util.data.groupEx
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.queryIntentActivitiesCompat import jp.juggler.util.queryIntentActivitiesCompat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withContext
import org.unifiedpush.android.connector.UnifiedPush
import java.util.ArrayList
private val log = LogCategory("ActMainIntent") private val log = LogCategory("ActMainIntent")
@ -154,21 +170,22 @@ fun ActMain.handleOtherUri(uri: Uri): Boolean {
return false return false
} }
private fun ActMain.handleCustomSchemaUri(uri: Uri) { private fun ActMain.handleCustomSchemaUri(uri: Uri) = launchAndShowError {
val dataIdString = uri.getQueryParameter("db_id") val dataIdString = uri.getQueryParameter("db_id")
if (dataIdString != null) { if (dataIdString == null) {
// subwaytooter://notification_click/?db_id=(db_id)
handleNotificationClick(uri, dataIdString)
} else {
// OAuth2 認証コールバック // OAuth2 認証コールバック
// subwaytooter://oauth(\d*)/?... // subwaytooter://oauth(\d*)/?...
handleOAuth2Callback(uri) handleOAuth2Callback(uri)
} else {
// subwaytooter://notification_click/?db_id=(db_id)
handleNotificationClick(uri, dataIdString)
} }
} }
private fun ActMain.handleNotificationClick(uri: Uri, dataIdString: String) { private fun ActMain.handleNotificationClick(uri: Uri, dataIdString: String) {
try { try {
val account = dataIdString.toLongOrNull()?.let { SavedAccount.loadAccount(this, it) } val account = dataIdString.toLongOrNull()
?.let { daoSavedAccount.loadAccount(it) }
if (account == null) { if (account == null) {
showToast(true, "handleNotificationClick: missing SavedAccount. id=$dataIdString") showToast(true, "handleNotificationClick: missing SavedAccount. id=$dataIdString")
return return
@ -227,7 +244,7 @@ fun ActMain.afterAccountVerify(auth2Result: Auth2Result): Boolean = auth2Result.
// 「アカウント追加のハズが既存アカウントで認証していた」 // 「アカウント追加のハズが既存アカウントで認証していた」
// 「アクセストークン更新のハズが別アカウントで認証していた」 // 「アクセストークン更新のハズが別アカウントで認証していた」
// などを防止するため、full acctでアプリ内DBを検索 // などを防止するため、full acctでアプリ内DBを検索
when (val sa = SavedAccount.loadAccountByAcct(this@afterAccountVerify, newAcct.ascii)) { when (val sa = daoSavedAccount.loadAccountByAcct(newAcct)) {
null -> afterAccountAdd(newAcct, auth2Result) null -> afterAccountAdd(newAcct, auth2Result)
else -> afterAccessTokenUpdate(auth2Result, sa) else -> afterAccessTokenUpdate(auth2Result, sa)
} }
@ -238,10 +255,10 @@ private fun ActMain.afterAccessTokenUpdate(
sa: SavedAccount, sa: SavedAccount,
): Boolean { ): Boolean {
// DBの情報を更新する // DBの情報を更新する
sa.updateTokenInfo(auth2Result) authRepo.updateTokenInfo(sa, auth2Result)
// 各カラムの持つアカウント情報をリロードする // 各カラムの持つアカウント情報をリロードする
reloadAccountSetting() reloadAccountSetting(daoSavedAccount.loadAccountList())
// 自動でリロードする // 自動でリロードする
appState.columnList appState.columnList
@ -252,6 +269,7 @@ private fun ActMain.afterAccessTokenUpdate(
PushSubscriptionHelper.clearLastCheck(sa) PushSubscriptionHelper.clearLastCheck(sa)
checkNotificationImmediateAll(this, onlySubscription = true) checkNotificationImmediateAll(this, onlySubscription = true)
checkNotificationImmediate(this, sa.db_id) checkNotificationImmediate(this, sa.db_id)
updatePushDistributer()
showToast(false, R.string.access_token_updated_for, sa.acct.pretty) showToast(false, R.string.access_token_updated_for, sa.acct.pretty)
return true return true
@ -263,7 +281,7 @@ private fun ActMain.afterAccountAdd(
): Boolean { ): Boolean {
val ta = auth2Result.tootAccount val ta = auth2Result.tootAccount
val rowId = SavedAccount.insert( val rowId = daoSavedAccount.saveNew(
acct = newAcct.ascii, acct = newAcct.ascii,
host = auth2Result.apiHost.ascii, host = auth2Result.apiHost.ascii,
domain = auth2Result.apDomain.ascii, domain = auth2Result.apDomain.ascii,
@ -271,7 +289,7 @@ private fun ActMain.afterAccountAdd(
token = auth2Result.tokenJson, token = auth2Result.tokenJson,
misskeyVersion = auth2Result.tootInstance.misskeyVersionMajor, misskeyVersion = auth2Result.tootInstance.misskeyVersionMajor,
) )
val account = SavedAccount.loadAccount(applicationContext, rowId) val account = daoSavedAccount.loadAccount(rowId)
if (account == null) { if (account == null) {
showToast(false, "loadAccount failed.") showToast(false, "loadAccount failed.")
return false return false
@ -298,13 +316,13 @@ private fun ActMain.afterAccountAdd(
} }
if (bModified) { if (bModified) {
account.saveSetting() daoSavedAccount.saveSetting(account)
} }
} }
// 適当にカラムを追加する // 適当にカラムを追加する
addColumn(false, defaultInsertPosition, account, ColumnType.HOME) addColumn(false, defaultInsertPosition, account, ColumnType.HOME)
if (SavedAccount.count == 1) { if (daoSavedAccount.isSingleAccount()) {
addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS) addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS)
addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL) addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL)
addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE) addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE)
@ -313,6 +331,7 @@ private fun ActMain.afterAccountAdd(
// 通知の更新が必要かもしれない // 通知の更新が必要かもしれない
checkNotificationImmediateAll(this, onlySubscription = true) checkNotificationImmediateAll(this, onlySubscription = true)
checkNotificationImmediate(this, account.db_id) checkNotificationImmediate(this, account.db_id)
updatePushDistributer()
showToast(false, R.string.account_confirmed) showToast(false, R.string.account_confirmed)
return true return true
} }
@ -329,3 +348,79 @@ fun ActMain.handleSharedIntent(intent: Intent) {
ai?.let { openActPostImpl(it.db_id, sharedIntent = intent) } ai?.let { openActPostImpl(it.db_id, sharedIntent = intent) }
} }
} }
// アカウントを追加/更新したらappServerHashの取得をやりなおす
fun ActMain.updatePushDistributer(){
when {
fcmHandler.noFcm && prefDevice.pushDistributor.isNullOrEmpty() -> {
try {
selectPushDistributor()
// 選択したら
} catch (_: CancellationException) {
// 選択しなかった場合は購読の更新を行わない
}
}
else -> PushWorker.enqueueRegisterEndpoint(this)
}
}
fun AppCompatActivity.selectPushDistributor() {
val context = this
launchAndShowError {
val prefDevice = prefDevice
val lastDistributor = prefDevice.pushDistributor
fun String.appendChecked(checked: Boolean) = when (checked) {
true -> "$this"
else -> this
}
actionsDialog(getString(R.string.select_push_delivery_service)) {
if (fcmHandler.hasFcm) {
action(
getString(R.string.firebase_cloud_messaging)
.appendChecked(lastDistributor == PrefDevice.PUSH_DISTRIBUTOR_FCM)
) {
runInProgress(cancellable = false) { reporter ->
withContext(AppDispatchers.DEFAULT) {
pushRepo.switchDistributor(
PrefDevice.PUSH_DISTRIBUTOR_FCM,
reporter = reporter
)
}
}
}
}
for (packageName in UnifiedPush.getDistributors(
context,
features = ArrayList(listOf(UnifiedPush.FEATURE_BYTES_MESSAGE))
)) {
action(
packageName.appendChecked(lastDistributor == packageName)
) {
runInProgress(cancellable = false) { reporter ->
withContext(AppDispatchers.DEFAULT) {
pushRepo.switchDistributor(
packageName,
reporter = reporter
)
}
}
}
}
action(
getString(R.string.none)
.appendChecked(lastDistributor == PrefDevice.PUSH_DISTRIBUTOR_NONE)
) {
runInProgress(cancellable = false) { reporter ->
withContext(AppDispatchers.DEFAULT) {
pushRepo.switchDistributor(
PrefDevice.PUSH_DISTRIBUTOR_NONE,
reporter = reporter
)
}
}
}
}
}
}

View File

@ -28,11 +28,11 @@ val ActMain.quickPostText: String
fun ActMain.initUIQuickPost() { fun ActMain.initUIQuickPost() {
etQuickPost.typeface = ActMain.timelineFont etQuickPost.typeface = ActMain.timelineFont
if (!PrefB.bpQuickPostBar.invoke(pref)) { if (!PrefB.bpQuickPostBar.value) {
llQuickPostBar.visibility = View.GONE llQuickPostBar.visibility = View.GONE
} }
if (PrefB.bpDontUseActionButtonWithQuickPostBar(pref)) { if (PrefB.bpDontUseActionButtonWithQuickPostBar.value) {
etQuickPost.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE etQuickPost.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
etQuickPost.imeOptions = EditorInfo.IME_ACTION_NONE etQuickPost.imeOptions = EditorInfo.IME_ACTION_NONE
// 最後に指定する必要がある? // 最後に指定する必要がある?
@ -70,7 +70,7 @@ fun ActMain.initUIQuickPost() {
fun ActMain.showQuickPostVisibility() { fun ActMain.showQuickPostVisibility() {
btnQuickPostMenu.imageResource = btnQuickPostMenu.imageResource =
when (val resId = getVisibilityIconId(false, quickPostVisibility)) { when (val resId = quickPostVisibility.getVisibilityIconId(false)) {
R.drawable.ic_question -> R.drawable.ic_description R.drawable.ic_question -> R.drawable.ic_description
else -> resId else -> resId
} }
@ -82,7 +82,7 @@ fun ActMain.toggleQuickPostMenu() {
fun ActMain.performQuickPost(account: SavedAccount?) { fun ActMain.performQuickPost(account: SavedAccount?) {
if (account == null) { if (account == null) {
val a = if (tabletViews != null && !PrefB.bpQuickTootOmitAccountSelection(pref)) { val a = if (tabletViews != null && !PrefB.bpQuickTootOmitAccountSelection.value) {
// タブレットモードでオプションが無効なら // タブレットモードでオプションが無効なら
// 簡易投稿は常にアカウント選択する // 簡易投稿は常にアカウント選択する
null null

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter.actmain package jp.juggler.subwaytooter.actmain
import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Typeface import android.graphics.Typeface
import android.view.View import android.view.View
@ -8,15 +9,15 @@ import android.widget.LinearLayout
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.stylerBoostAlpha
import jp.juggler.subwaytooter.itemviewholder.ItemViewHolder import jp.juggler.subwaytooter.itemviewholder.ItemViewHolder
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefF import jp.juggler.subwaytooter.pref.PrefF
import jp.juggler.subwaytooter.pref.PrefI import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.PrefS import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.pref.impl.StringPref import jp.juggler.subwaytooter.pref.impl.StringPref
import jp.juggler.subwaytooter.stylerRoundRatio
import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.stylerBoostAlpha
import jp.juggler.subwaytooter.stylerRoundRatio
import jp.juggler.subwaytooter.util.CustomShare import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.view.ListDivider import jp.juggler.subwaytooter.view.ListDivider
import jp.juggler.util.data.clip import jp.juggler.util.data.clip
@ -31,12 +32,12 @@ import kotlin.math.max
private val log = LogCategory("ActMainStyle") private val log = LogCategory("ActMainStyle")
private fun ActMain.dpToPx(dp: Float) = private fun Float.dpToPx(context: Context) =
(dp * density + 0.5f).toInt() (this * context.resources.displayMetrics.density + 0.5f).toInt()
// initUIから呼ばれる // initUIから呼ばれる
fun ActMain.reloadFonts() { fun reloadFonts() {
ActMain.timelineFont = PrefS.spTimelineFont(pref).notEmpty()?.let { ActMain.timelineFont = PrefS.spTimelineFont.value.notEmpty()?.let {
try { try {
Typeface.createFromFile(it) Typeface.createFromFile(it)
} catch (ex: Throwable) { } catch (ex: Throwable) {
@ -45,7 +46,7 @@ fun ActMain.reloadFonts() {
} }
} ?: Typeface.DEFAULT } ?: Typeface.DEFAULT
ActMain.timelineFontBold = PrefS.spTimelineFontBold(pref).notEmpty()?.let { ActMain.timelineFontBold = PrefS.spTimelineFontBold.value.notEmpty()?.let {
try { try {
Typeface.createFromFile(it) Typeface.createFromFile(it)
} catch (ex: Throwable) { } catch (ex: Throwable) {
@ -61,16 +62,14 @@ fun ActMain.reloadFonts() {
} }
private fun ActMain.parseIconSize(stringPref: StringPref, minDp: Float = 1f) = private fun ActMain.parseIconSize(stringPref: StringPref, minDp: Float = 1f) =
dpToPx( (try {
try { stringPref.value
stringPref(pref) .toFloatOrNull()
.toFloatOrNull() ?.takeIf { it.isFinite() && it >= minDp }
?.takeIf { it.isFinite() && it >= minDp } } catch (ex: Throwable) {
} catch (ex: Throwable) { log.e(ex, "parseIconSize failed.")
log.e(ex, "parseIconSize failed.") null
null } ?: stringPref.defVal.toFloat()).dpToPx(this)
} ?: stringPref.defVal.toFloat()
)
// initUIから呼ばれる // initUIから呼ばれる
fun ActMain.reloadIconSize() { fun ActMain.reloadIconSize() {
@ -82,7 +81,7 @@ fun ActMain.reloadIconSize() {
ActMain.stripIconSize = parseIconSize(PrefS.spStripIconSize) ActMain.stripIconSize = parseIconSize(PrefS.spStripIconSize)
ActMain.screenBottomPadding = parseIconSize(PrefS.spScreenBottomPadding, minDp = 0f) ActMain.screenBottomPadding = parseIconSize(PrefS.spScreenBottomPadding, minDp = 0f)
ActMain.eventFadeAlpha = PrefS.spEventTextAlpha() ActMain.eventFadeAlpha = PrefS.spEventTextAlpha.value
.toFloatOrNull() .toFloatOrNull()
?.takeIf { it.isFinite() } ?.takeIf { it.isFinite() }
?.clip(0f, 1f) ?.clip(0f, 1f)
@ -90,10 +89,10 @@ fun ActMain.reloadIconSize() {
} }
// initUIから呼ばれる // initUIから呼ばれる
fun ActMain.reloadRoundRatio() { fun reloadRoundRatio() {
val sizeDp = when { val sizeDp = when {
PrefB.bpDontRound(pref) -> 0f PrefB.bpDontRound.value -> 0f
else -> PrefS.spRoundRatio(pref) else -> PrefS.spRoundRatio.value
.toFloatOrNull() .toFloatOrNull()
?.takeIf { it.isFinite() } ?.takeIf { it.isFinite() }
?: 33f ?: 33f
@ -102,8 +101,8 @@ fun ActMain.reloadRoundRatio() {
} }
// initUI から呼ばれる // initUI から呼ばれる
fun ActMain.reloadBoostAlpha() { fun reloadBoostAlpha() {
stylerBoostAlpha = PrefS.spBoostAlpha(pref) stylerBoostAlpha = PrefS.spBoostAlpha.value
.toIntOrNull() .toIntOrNull()
?.toFloat() ?.toFloat()
?.let { (it + 0.5f) / 100f } ?.let { (it + 0.5f) / 100f }
@ -112,36 +111,35 @@ fun ActMain.reloadBoostAlpha() {
} }
fun ActMain.reloadMediaHeight() { fun ActMain.reloadMediaHeight() {
appState.mediaThumbHeight = dpToPx( appState.mediaThumbHeight = (
PrefS.spMediaThumbHeight(pref) PrefS.spMediaThumbHeight.value
.toFloatOrNull() .toFloatOrNull()
?.takeIf { it >= 32f } ?.takeIf { it >= 32f }
?: 64f ?: 64f
) ).dpToPx(this)
} }
private fun Float.clipFontSize(): Float = private fun Float.clipFontSize(): Float =
if (isNaN()) this else max(1f, this) if (isNaN()) this else max(1f, this)
fun ActMain.reloadTextSize() { fun ActMain.reloadTextSize() {
timelineFontSizeSp = PrefF.fpTimelineFontSize.invoke(pref).clipFontSize() timelineFontSizeSp = PrefF.fpTimelineFontSize.value.clipFontSize()
acctFontSizeSp = PrefF.fpAcctFontSize(pref).clipFontSize() acctFontSizeSp = PrefF.fpAcctFontSize.value.clipFontSize()
notificationTlFontSizeSp = PrefF.fpNotificationTlFontSize(pref).clipFontSize() notificationTlFontSizeSp = PrefF.fpNotificationTlFontSize.value.clipFontSize()
headerTextSizeSp = PrefF.fpHeaderTextSize(pref).clipFontSize() headerTextSizeSp = PrefF.fpHeaderTextSize.value.clipFontSize()
val fv = PrefS.spTimelineSpacing(pref).toFloatOrNull() val fv = PrefS.spTimelineSpacing.value.toFloatOrNull()
timelineSpacing = if (fv != null && fv.isFinite() && fv != 0f) fv else null timelineSpacing = if (fv != null && fv.isFinite() && fv != 0f) fv else null
} }
fun ActMain.loadColumnMin() = fun ActMain.loadColumnMin() =
dpToPx( (PrefS.spColumnWidth.value
PrefS.spColumnWidth(pref) .toFloatOrNull()
.toFloatOrNull() ?.takeIf { it.isFinite() && it >= 100f }
?.takeIf { it.isFinite() && it >= 100f } ?: ActMain.COLUMN_WIDTH_MIN_DP.toFloat()
?: ActMain.COLUMN_WIDTH_MIN_DP.toFloat() ).dpToPx(this)
)
fun ActMain.justifyWindowContentPortrait() { fun ActMain.justifyWindowContentPortrait() {
when (PrefI.ipJustifyWindowContentPortrait(pref)) { when (PrefI.ipJustifyWindowContentPortrait.value) {
PrefI.JWCP_START -> { PrefI.JWCP_START -> {
val iconW = (ActMain.stripIconSize * 1.5f + 0.5f).toInt() val iconW = (ActMain.stripIconSize * 1.5f + 0.5f).toInt()
val padding = resources.displayMetrics.widthPixels / 2 - iconW val padding = resources.displayMetrics.widthPixels / 2 - iconW
@ -161,7 +159,7 @@ fun ActMain.justifyWindowContentPortrait() {
PrefI.JWCP_END -> { PrefI.JWCP_END -> {
val iconW = (ActMain.stripIconSize * 1.5f + 0.5f).toInt() val iconW = (ActMain.stripIconSize * 1.5f + 0.5f).toInt()
val borderWidth = dpToPx(1f) val borderWidth = 1f.dpToPx(this)
val padding = resources.displayMetrics.widthPixels / 2 - iconW - borderWidth val padding = resources.displayMetrics.widthPixels / 2 - iconW - borderWidth
fun ViewGroup.addViewAfterFirst(v: View) = addView(v, 1) fun ViewGroup.addViewAfterFirst(v: View) = addView(v, 1)
@ -182,10 +180,10 @@ fun ActMain.justifyWindowContentPortrait() {
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
// onStart時に呼ばれる // onStart時に呼ばれる
fun ActMain.reloadTimeZone() { fun reloadTimeZone() {
try { try {
var tz = TimeZone.getDefault() var tz = TimeZone.getDefault()
val tzId = PrefS.spTimeZone(pref) val tzId = PrefS.spTimeZone.value
if (tzId.isNotEmpty()) { if (tzId.isNotEmpty()) {
tz = TimeZone.getTimeZone(tzId) tz = TimeZone.getTimeZone(tzId)
} }
@ -199,25 +197,25 @@ fun ActMain.reloadTimeZone() {
// onStart時に呼ばれる // onStart時に呼ばれる
// カラーカスタマイズを読み直す // カラーカスタマイズを読み直す
fun ActMain.reloadColors() { fun ActMain.reloadColors() {
ListDivider.color = PrefI.ipListDividerColor(pref) ListDivider.color = PrefI.ipListDividerColor.value
TabletColumnDivider.color = PrefI.ipListDividerColor(pref) TabletColumnDivider.color = PrefI.ipListDividerColor.value
ItemViewHolder.toot_color_unlisted = PrefI.ipTootColorUnlisted(pref) ItemViewHolder.toot_color_unlisted = PrefI.ipTootColorUnlisted.value
ItemViewHolder.toot_color_follower = PrefI.ipTootColorFollower(pref) ItemViewHolder.toot_color_follower = PrefI.ipTootColorFollower.value
ItemViewHolder.toot_color_direct_user = PrefI.ipTootColorDirectUser(pref) ItemViewHolder.toot_color_direct_user = PrefI.ipTootColorDirectUser.value
ItemViewHolder.toot_color_direct_me = PrefI.ipTootColorDirectMe(pref) ItemViewHolder.toot_color_direct_me = PrefI.ipTootColorDirectMe.value
MyClickableSpan.showLinkUnderline = PrefB.bpShowLinkUnderline(pref) MyClickableSpan.showLinkUnderline = PrefB.bpShowLinkUnderline.value
MyClickableSpan.defaultLinkColor = PrefI.ipLinkColor(pref).notZero() MyClickableSpan.defaultLinkColor = PrefI.ipLinkColor.value.notZero()
?: attrColor(R.attr.colorLink) ?: attrColor(R.attr.colorLink)
CustomShare.reloadCache(this) CustomShare.reloadCache(this)
} }
fun ActMain.showFooterColor() { fun ActMain.showFooterColor() {
val footerButtonBgColor = PrefI.ipFooterButtonBgColor(pref) val footerButtonBgColor = PrefI.ipFooterButtonBgColor.value
val footerButtonFgColor = PrefI.ipFooterButtonFgColor(pref) val footerButtonFgColor = PrefI.ipFooterButtonFgColor.value
val footerTabBgColor = PrefI.ipFooterTabBgColor(pref) val footerTabBgColor = PrefI.ipFooterTabBgColor.value
val footerTabDividerColor = PrefI.ipFooterTabDividerColor(pref) val footerTabDividerColor = PrefI.ipFooterTabDividerColor.value
val footerTabIndicatorColor = PrefI.ipFooterTabIndicatorColor(pref) val footerTabIndicatorColor = PrefI.ipFooterTabIndicatorColor.value
val colorColumnStripBackground = footerTabBgColor.notZero() val colorColumnStripBackground = footerTabBgColor.notZero()
?: attrColor(R.attr.colorColumnStripBackground) ?: attrColor(R.attr.colorColumnStripBackground)

View File

@ -3,18 +3,23 @@ package jp.juggler.subwaytooter.actmain
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.view.MyViewPager import jp.juggler.subwaytooter.view.MyViewPager
// スマホモードならラムダを実行する。タブレットモードならnullを返す // スマホモードならラムダを実行する。タブレットモードならnullを返す
inline fun <R : Any?> ActMain.phoneOnly(code: (ActMainPhoneViews) -> R): R? = phoneViews?.let { code(it) } inline fun <R : Any?> ActMain.phoneOnly(code: (ActMainPhoneViews) -> R): R? =
phoneViews?.let { code(it) }
// タブレットモードならラムダを実行する。スマホモードならnullを返す // タブレットモードならラムダを実行する。スマホモードならnullを返す
inline fun <R : Any?> ActMain.tabOnly(code: (ActMainTabletViews) -> R): R? = tabletViews?.let { code(it) } inline fun <R : Any?> ActMain.tabOnly(code: (ActMainTabletViews) -> R): R? =
tabletViews?.let { code(it) }
// スマホモードとタブレットモードでコードを切り替える // スマホモードとタブレットモードでコードを切り替える
inline fun <R : Any?> ActMain.phoneTab(codePhone: (ActMainPhoneViews) -> R, codeTablet: (ActMainTabletViews) -> R): R { inline fun <R : Any?> ActMain.phoneTab(
codePhone: (ActMainPhoneViews) -> R,
codeTablet: (ActMainTabletViews) -> R,
): R {
phoneViews?.let { return codePhone(it) } phoneViews?.let { return codePhone(it) }
tabletViews?.let { return codeTablet(it) } tabletViews?.let { return codeTablet(it) }
error("missing phoneViews/tabletViews") error("missing phoneViews/tabletViews")
@ -27,7 +32,7 @@ fun ActMain.initPhoneTablet() {
val tmpTabletPager: RecyclerView = findViewById(R.id.rvPager) val tmpTabletPager: RecyclerView = findViewById(R.id.rvPager)
// スマホモードとタブレットモードの切り替え // スマホモードとタブレットモードの切り替え
if (PrefB.bpDisableTabletMode(pref) || sw < columnWMin * 2) { if (PrefB.bpDisableTabletMode.value || sw < columnWMin * 2) {
tmpTabletPager.visibility = View.GONE tmpTabletPager.visibility = View.GONE
phoneViews = ActMainPhoneViews(this).apply { phoneViews = ActMainPhoneViews(this).apply {
initUI(tmpPhonePager) initUI(tmpPhonePager)

View File

@ -92,7 +92,7 @@ class ActMainTabletViews(val actMain: ActMain) {
// if( animator is DefaultItemAnimator){ // if( animator is DefaultItemAnimator){
// animator.supportsChangeAnimations = false // animator.supportsChangeAnimations = false
// } // }
if (PrefB.bpTabletSnap()) { if (PrefB.bpTabletSnap.value) {
GravitySnapHelper(Gravity.START).attachToRecyclerView(this.tabletPager) GravitySnapHelper(Gravity.START).attachToRecyclerView(this.tabletPager)
} }
} }

View File

@ -28,11 +28,12 @@ import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefS import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.accountListCanSeeMyReactions
import jp.juggler.subwaytooter.util.VersionString import jp.juggler.subwaytooter.util.VersionString
import jp.juggler.subwaytooter.util.openBrowser import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.util.coroutine.AppDispatchers import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchIO import jp.juggler.util.coroutine.launchIO
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.decodeJsonObject import jp.juggler.util.data.decodeJsonObject
import jp.juggler.util.data.decodeUTF8 import jp.juggler.util.data.decodeUTF8
@ -120,7 +121,7 @@ class SideMenuAdapter(
) )
) )
val newRelease = releaseInfo?.jsonObject( val newRelease = releaseInfo?.jsonObject(
if (PrefB.bpCheckBetaVersion()) "beta" else "stable" if (PrefB.bpCheckBetaVersion.value) "beta" else "stable"
) )
// 使用中のアプリバージョンより新しいリリースがある? // 使用中のアプリバージョンより新しいリリースがある?
@ -327,7 +328,7 @@ class SideMenuAdapter(
timeline(defaultInsertPosition, ColumnType.BOOKMARKS) timeline(defaultInsertPosition, ColumnType.BOOKMARKS)
}, },
Item(icon = R.drawable.ic_face, title = R.string.reactioned_posts) { Item(icon = R.drawable.ic_face, title = R.string.reactioned_posts) {
launchMain { launchAndShowError {
accountListCanSeeMyReactions()?.let { list -> accountListCanSeeMyReactions()?.let { list ->
if (list.isEmpty()) { if (list.isEmpty()) {
showToast(false, R.string.not_available_for_current_accounts) showToast(false, R.string.not_available_for_current_accounts)
@ -518,7 +519,7 @@ class SideMenuAdapter(
private fun getTimeZoneString(context: Context): String { private fun getTimeZoneString(context: Context): String {
try { try {
var tz = TimeZone.getDefault() var tz = TimeZone.getDefault()
val tzId = PrefS.spTimeZone() val tzId = PrefS.spTimeZone.value
if (tzId.isBlank()) { if (tzId.isBlank()) {
return tz.displayName + "(" + context.getString(R.string.device_timezone) + ")" return tz.displayName + "(" + context.getString(R.string.device_timezone) + ")"
} }

View File

@ -5,8 +5,10 @@ import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TootVisibility import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.table.sortedByNickname
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.notZero import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -33,17 +35,17 @@ fun ActPost.selectAccount(a: SavedAccount?) {
views.spLanguage.setSelection(max(0, languages.indexOfFirst { it.first == a.lang })) views.spLanguage.setSelection(max(0, languages.indexOfFirst { it.first == a.lang }))
val ac = AcctColor.load(a) val ac = daoAcctColor.load(a)
views.btnAccount.text = ac.nickname views.btnAccount.text = ac.nickname
if (AcctColor.hasColorBackground(ac)) { if (daoAcctColor.hasColorBackground(ac)) {
views.btnAccount.background = views.btnAccount.background =
getAdaptiveRippleDrawableRound(this, ac.color_bg, ac.color_fg) getAdaptiveRippleDrawableRound(this, ac.colorBg, ac.colorFg)
} else { } else {
views.btnAccount.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp) views.btnAccount.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
} }
views.btnAccount.textColor = ac.color_fg.notZero() views.btnAccount.textColor = ac.colorFg.notZero()
?: attrColor(android.R.attr.textColorPrimary) ?: attrColor(android.R.attr.textColorPrimary)
} }
updateTextCount() updateTextCount()
@ -75,8 +77,7 @@ fun ActPost.performAccountChooser() {
if (!canSwitchAccount()) return if (!canSwitchAccount()) return
if (isMultiWindowPost) { if (isMultiWindowPost) {
accountList = SavedAccount.loadAccountList(this) accountList = daoSavedAccount.loadAccountList().sortedByNickname()
SavedAccount.sort(accountList)
} }
launchMain { launchMain {

View File

@ -12,13 +12,14 @@ import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.calcIconRound import jp.juggler.subwaytooter.calcIconRound
import jp.juggler.subwaytooter.defaultColorIcon import jp.juggler.subwaytooter.defaultColorIcon
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgFocusPoint import jp.juggler.subwaytooter.dialog.DlgFocusPoint
import jp.juggler.subwaytooter.dialog.DlgTextInput import jp.juggler.subwaytooter.dialog.DlgTextInput
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.util.AttachmentRequest import jp.juggler.subwaytooter.util.AttachmentRequest
import jp.juggler.subwaytooter.util.PostAttachment import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.subwaytooter.view.MyNetworkImageView import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -169,7 +170,7 @@ fun ActPost.onPostAttachmentCompleteImpl(pa: PostAttachment) {
log.i("onPostAttachmentComplete: upload complete.") log.i("onPostAttachmentComplete: upload complete.")
// 投稿欄の末尾に追記する // 投稿欄の末尾に追記する
if (PrefB.bpAppendAttachmentUrlToContent.invoke(pref)) { if (PrefB.bpAppendAttachmentUrlToContent.value) {
appendArrachmentUrl(a) appendArrachmentUrl(a)
} }
} }
@ -215,35 +216,33 @@ fun ActPost.performAttachmentClick(idx: Int) {
showToast(false, ex.withCaption("can't get attachment item[$idx].")) showToast(false, ex.withCaption("can't get attachment item[$idx]."))
return return
} }
launchAndShowError {
val a = ActionsDialog() actionsDialog(getString(R.string.media_attachment)) {
.addAction(getString(R.string.set_description)) { action(getString(R.string.set_description)) {
editAttachmentDescription(pa) editAttachmentDescription(pa)
}
if (pa.attachment?.canFocus == true) {
a.addAction(getString(R.string.set_focus_point)) {
openFocusPoint(pa)
}
}
if (account?.isMastodon == true) {
when (pa.attachment?.type) {
TootAttachmentType.Audio,
TootAttachmentType.GIFV,
TootAttachmentType.Video,
-> a.addAction(getString(R.string.custom_thumbnail)) {
attachmentPicker.openCustomThumbnail(pa)
} }
if (pa.attachment?.canFocus == true) {
action(getString(R.string.set_focus_point)) {
openFocusPoint(pa)
}
}
if (account?.isMastodon == true) {
when (pa.attachment?.type) {
TootAttachmentType.Audio,
TootAttachmentType.GIFV,
TootAttachmentType.Video,
-> action(getString(R.string.custom_thumbnail)) {
attachmentPicker.openCustomThumbnail(pa)
}
else -> Unit else -> Unit
}
}
action(getString(R.string.delete)) {
deleteAttachment(pa)
}
} }
} }
a.addAction(getString(R.string.delete)) {
deleteAttachment(pa)
}
a.show(this, title = getString(R.string.media_attachment))
} }
fun ActPost.deleteAttachment(pa: PostAttachment) { fun ActPost.deleteAttachment(pa: PostAttachment) {

View File

@ -9,8 +9,9 @@ import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.dialog.DlgDraftPicker import jp.juggler.subwaytooter.dialog.DlgDraftPicker
import jp.juggler.subwaytooter.table.PostDraft
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoPostDraft
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.PostAttachment import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.util.coroutine.launchProgress import jp.juggler.util.coroutine.launchProgress
@ -129,7 +130,7 @@ fun ActPost.saveDraft() {
states.visibility?.id?.toString()?.let { json.put(DRAFT_VISIBILITY, it) } states.visibility?.id?.toString()?.let { json.put(DRAFT_VISIBILITY, it) }
states.inReplyToId?.putTo(json, DRAFT_REPLY_ID) states.inReplyToId?.putTo(json, DRAFT_REPLY_ID)
PostDraft.save(System.currentTimeMillis(), json) daoPostDraft.save(System.currentTimeMillis(), json)
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.e(ex, "saveDraft failed.") log.e(ex, "saveDraft failed.")
} }
@ -152,7 +153,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
draft.jsonArray(DRAFT_ATTACHMENT_LIST)?.objectList()?.toMutableList() draft.jsonArray(DRAFT_ATTACHMENT_LIST)?.objectList()?.toMutableList()
val accountDbId = draft.long(DRAFT_ACCOUNT_DB_ID) ?: -1L val accountDbId = draft.long(DRAFT_ACCOUNT_DB_ID) ?: -1L
val account = SavedAccount.loadAccount(this@restoreDraft, accountDbId) val account = daoSavedAccount.loadAccount(accountDbId)
if (account == null) { if (account == null) {
listWarning.add(getString(R.string.account_in_draft_is_lost)) listWarning.add(getString(R.string.account_in_draft_is_lost))
try { try {

View File

@ -1,7 +1,6 @@
package jp.juggler.subwaytooter.actpost package jp.juggler.subwaytooter.actpost
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.ActPost import jp.juggler.subwaytooter.ActPost
@ -11,16 +10,20 @@ import jp.juggler.subwaytooter.actmain.onCompleteActPost
import jp.juggler.subwaytooter.api.entity.TootPollsType import jp.juggler.subwaytooter.api.entity.TootPollsType
import jp.juggler.subwaytooter.api.entity.TootVisibility import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.api.entity.unknownHostAndDomain import jp.juggler.subwaytooter.api.entity.unknownHostAndDomain
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.PostDraft
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoPostDraft
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.table.sortedByNickname
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.PostAttachment import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.subwaytooter.util.PostImpl import jp.juggler.subwaytooter.util.PostImpl
import jp.juggler.subwaytooter.util.PostResult import jp.juggler.subwaytooter.util.PostResult
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.CharacterGroup import jp.juggler.util.data.CharacterGroup
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
@ -104,27 +107,27 @@ fun ActPost.hasContent(): Boolean {
} }
fun ActPost.resetText() { fun ActPost.resetText() {
isPostComplete = false launchMain {
isPostComplete = false
resetReply() resetReply()
resetMushroom() resetMushroom()
states.redraftStatusId = null states.redraftStatusId = null
states.editStatusId = null states.editStatusId = null
states.timeSchedule = 0L states.timeSchedule = 0L
attachmentPicker.reset() attachmentPicker.reset()
scheduledStatus = null scheduledStatus = null
attachmentList.clear() attachmentList.clear()
views.cbQuote.isChecked = false views.cbQuote.isChecked = false
views.etContent.setText("") views.etContent.setText("")
views.spPollType.setSelection(0, false) views.spPollType.setSelection(0, false)
etChoices.forEach { it.setText("") } etChoices.forEach { it.setText("") }
accountList = SavedAccount.loadAccountList(this) accountList = daoSavedAccount.loadAccountList().sortedByNickname()
SavedAccount.sort(accountList) if (accountList.isEmpty()) {
if (accountList.isEmpty()) { showToast(true, R.string.please_add_account)
showToast(true, R.string.please_add_account) finish()
finish() }
return
} }
} }
@ -148,28 +151,18 @@ fun ActPost.afterUpdateText() {
} }
// 初期化時と投稿完了時とリセット確認後に呼ばれる // 初期化時と投稿完了時とリセット確認後に呼ばれる
fun ActPost.updateText( suspend fun ActPost.updateText(
intent: Intent, intent: Intent,
confirmed: Boolean = false,
saveDraft: Boolean = true, saveDraft: Boolean = true,
resetAccount: Boolean = true, resetAccount: Boolean = true,
) { ) {
if (!canSwitchAccount()) return if (!canSwitchAccount()) return
if (!confirmed && hasContent()) { if (saveDraft && hasContent()) {
AlertDialog.Builder(this) confirm(R.string.post_reset_confirm)
.setMessage("編集中のテキストや文脈を下書きに退避して、新しい投稿を編集しますか? ") saveDraft()
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
updateText(intent, confirmed = true)
}
.setCancelable(true)
.show()
return
} }
if (saveDraft) saveDraft()
resetText() resetText()
// Android 9 から、明示的にフォーカスを当てる必要がある // Android 9 から、明示的にフォーカスを当てる必要がある
@ -262,7 +255,7 @@ fun ActPost.initializeFromSharedIntent(sharedIntent: Intent) {
else -> false else -> false
} }
if (!hasUri || !PrefB.bpIgnoreTextInSharedMedia(pref)) { if (!hasUri || !PrefB.bpIgnoreTextInSharedMedia.value) {
appendContentText(sharedIntent) appendContentText(sharedIntent)
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
@ -271,33 +264,33 @@ fun ActPost.initializeFromSharedIntent(sharedIntent: Intent) {
} }
fun ActPost.performMore() { fun ActPost.performMore() {
val dialog = ActionsDialog() launchAndShowError {
actionsDialog {
action(getString(R.string.open_picker_emoji)) {
completionHelper.openEmojiPickerFromMore()
}
dialog.addAction(getString(R.string.open_picker_emoji)) { action(getString(R.string.clear_text)) {
completionHelper.openEmojiPickerFromMore() views.etContent.setText("")
views.etContentWarning.setText("")
}
action(getString(R.string.clear_text_and_media)) {
views.etContent.setText("")
views.etContentWarning.setText("")
attachmentList.clear()
showMediaAttachment()
}
if (daoPostDraft.hasDraft()) action(getString(R.string.restore_draft)) {
openDraftPicker()
}
action(getString(R.string.recommended_plugin)) {
showRecommendedPlugin(null)
}
}
} }
dialog.addAction(getString(R.string.clear_text)) {
views.etContent.setText("")
views.etContentWarning.setText("")
}
dialog.addAction(getString(R.string.clear_text_and_media)) {
views.etContent.setText("")
views.etContentWarning.setText("")
attachmentList.clear()
showMediaAttachment()
}
if (PostDraft.hasDraft()) dialog.addAction(getString(R.string.restore_draft)) {
openDraftPicker()
}
dialog.addAction(getString(R.string.recommended_plugin)) {
showRecommendedPlugin(null)
}
dialog.show(this, null)
} }
fun ActPost.performPost() { fun ActPost.performPost() {
@ -366,7 +359,7 @@ fun ActPost.performPost() {
if (isMultiWindowPost) { if (isMultiWindowPost) {
resetText() resetText()
updateText(Intent(), confirmed = true, saveDraft = false, resetAccount = false) updateText(Intent(), saveDraft = false, resetAccount = false)
afterUpdateText() afterUpdateText()
} else { } else {
// ActMainの復元が必要な場合に備えてintentのdataでも渡す // ActMainの復元が必要な場合に備えてintentのdataでも渡す
@ -382,7 +375,7 @@ fun ActPost.performPost() {
if (isMultiWindowPost) { if (isMultiWindowPost) {
resetText() resetText()
updateText(Intent(), confirmed = true, saveDraft = false, resetAccount = false) updateText(Intent(), saveDraft = false, resetAccount = false)
afterUpdateText() afterUpdateText()
ActMain.refActMain?.get()?.onCompleteActPost(data) ActMain.refActMain?.get()?.onCompleteActPost(data)
} else { } else {

View File

@ -10,10 +10,8 @@ import jp.juggler.subwaytooter.getVisibilityCaption
import jp.juggler.subwaytooter.getVisibilityIconId import jp.juggler.subwaytooter.getVisibilityIconId
fun ActPost.showVisibility() { fun ActPost.showVisibility() {
val iconId = getVisibilityIconId( val iconId = (states.visibility ?: TootVisibility.Public)
account?.isMisskey == true, .getVisibilityIconId(account?.isMisskey == true)
states.visibility ?: TootVisibility.Public
)
views.btnVisibility.setImageResource(iconId) views.btnVisibility.setImageResource(iconId)
} }

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.actpost package jp.juggler.subwaytooter.actpost
import android.content.SharedPreferences
import android.os.Handler import android.os.Handler
import android.text.* import android.text.*
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
@ -9,21 +8,20 @@ import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TootTag import jp.juggler.subwaytooter.api.entity.TootTag
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.launchEmojiPicker import jp.juggler.subwaytooter.dialog.launchEmojiPicker
import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.EmojiBase import jp.juggler.subwaytooter.emoji.EmojiBase
import jp.juggler.subwaytooter.emoji.UnicodeEmoji import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.NetworkEmojiSpan import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.table.AcctSet import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.TagSet
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.EmojiDecoder import jp.juggler.subwaytooter.util.EmojiDecoder
import jp.juggler.subwaytooter.util.PopupAutoCompleteAcct import jp.juggler.subwaytooter.util.PopupAutoCompleteAcct
import jp.juggler.subwaytooter.view.MyEditText import jp.juggler.subwaytooter.view.MyEditText
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.asciiRegex import jp.juggler.util.data.asciiRegex
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.attrColor import jp.juggler.util.ui.attrColor
@ -33,7 +31,6 @@ import kotlin.math.min
// 入力補完機能 // 入力補完機能
class CompletionHelper( class CompletionHelper(
private val activity: AppCompatActivity, private val activity: AppCompatActivity,
private val pref: SharedPreferences,
private val handler: Handler, private val handler: Handler,
) { ) {
companion object { companion object {
@ -161,7 +158,7 @@ class CompletionHelper(
val limit = 100 val limit = 100
val s = src.substring(start, end) val s = src.substring(start, end)
val acctList = AcctSet.searchPrefix(s, limit) val acctList = daoAcctSet.searchPrefix(s, limit)
log.d("search for $s, result=${acctList.size}") log.d("search for $s, result=${acctList.size}")
if (acctList.isEmpty()) { if (acctList.isEmpty()) {
closeAcctPopup() closeAcctPopup()
@ -187,7 +184,7 @@ class CompletionHelper(
val limit = 100 val limit = 100
val s = src.substring(lastSharp + 1, end) val s = src.substring(lastSharp + 1, end)
val tagList = TagSet.searchPrefix(s, limit) val tagList = daoTagHistory.searchPrefix(s, limit)
log.d("search for $s, result=${tagList.size}") log.d("search for $s, result=${tagList.size}")
if (tagList.isEmpty()) { if (tagList.isEmpty()) {
closeAcctPopup() closeAcctPopup()
@ -448,7 +445,7 @@ class CompletionHelper(
launchEmojiPicker( launchEmojiPicker(
activity, activity,
accessInfo, accessInfo,
closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref) closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected.value
) { emoji, bInstanceHasCustomEmoji -> ) { emoji, bInstanceHasCustomEmoji ->
val et = this@CompletionHelper.et ?: return@launchEmojiPicker val et = this@CompletionHelper.et ?: return@launchEmojiPicker
@ -482,7 +479,7 @@ class CompletionHelper(
launchEmojiPicker( launchEmojiPicker(
activity, activity,
accessInfo, accessInfo,
closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref) closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected.value
) { emoji, bInstanceHasCustomEmoji -> ) { emoji, bInstanceHasCustomEmoji ->
val et = this@CompletionHelper.et ?: return@launchEmojiPicker val et = this@CompletionHelper.et ?: return@launchEmojiPicker
@ -514,49 +511,50 @@ class CompletionHelper(
} }
fun openFeaturedTagList(list: List<TootTag>?) { fun openFeaturedTagList(list: List<TootTag>?) {
val ad = ActionsDialog() val et = this@CompletionHelper.et ?: return
list?.forEach { tag -> activity.run {
ad.addAction("#${tag.name}") { launchAndShowError {
val et = this.et ?: return@addAction actionsDialog(getString(R.string.featured_hashtags)) {
list?.forEach { tag ->
action("#${tag.name}") {
val src = et.text ?: ""
val srcLength = src.length
val start = min(srcLength, et.selectionStart)
val end = min(srcLength, et.selectionEnd)
val src = et.text ?: "" val sb = SpannableStringBuilder()
val srcLength = src.length .append(src.subSequence(0, start))
val start = min(srcLength, et.selectionStart) .appendHashTag(tag.name)
val end = min(srcLength, et.selectionEnd) val newSelection = sb.length
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
val sb = SpannableStringBuilder() et.text = sb
.append(src.subSequence(0, start)) et.setSelection(newSelection)
.appendHashTag(tag.name)
val newSelection = sb.length
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
et.text = sb procTextChanged.run()
et.setSelection(newSelection) }
}
action(activity.getString(R.string.input_sharp_itself)) {
val src = et.text ?: ""
val srcLength = src.length
val start = min(srcLength, et.selectionStart)
val end = min(srcLength, et.selectionEnd)
procTextChanged.run() val sb = SpannableStringBuilder()
sb.append(src.subSequence(0, start))
if (!EmojiDecoder.canStartHashtag(sb, sb.length)) sb.append(' ')
sb.append('#')
val newSelection = sb.length
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
et.text = sb
et.setSelection(newSelection)
procTextChanged.run()
}
}
} }
} }
ad.addAction(activity.getString(R.string.input_sharp_itself)) {
val et = this.et ?: return@addAction
val src = et.text ?: ""
val srcLength = src.length
val start = min(srcLength, et.selectionStart)
val end = min(srcLength, et.selectionEnd)
val sb = SpannableStringBuilder()
sb.append(src.subSequence(0, start))
if (!EmojiDecoder.canStartHashtag(sb, sb.length)) sb.append(' ')
sb.append('#')
val newSelection = sb.length
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
et.text = sb
et.setSelection(newSelection)
procTextChanged.run()
}
ad.show(activity, activity.getString(R.string.featured_hashtags))
} }
// final ActionMode.Callback action_mode_callback = new ActionMode.Callback() { // final ActionMode.Callback action_mode_callback = new ActionMode.Callback() {

View File

@ -0,0 +1,206 @@
package jp.juggler.subwaytooter.api
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.withCaption
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.closeQuietly
import ru.gildor.coroutines.okhttp.await
import java.io.IOException
private val log = LogCategory("ApiUtils2")
const val JSON_SERVER_TYPE = "<>serverType"
const val SERVER_MISSKEY = "misskey"
const val SERVER_MASTODON = "mastodon"
val DEFAULT_JSON_ERROR_PARSER =
{ json: JsonObject -> json["error"]?.toString() }
private val reWhiteSpace = """\s+""".toRegex()
private val reStartJsonArray = """\A\s*\[""".toRegex()
private val reStartJsonObject = """\A\s*\{""".toRegex()
fun Request.Builder.authorizationBearer(token: String?) =
apply { token.notEmpty()?.let { header("Authorization", "Bearer $it") } }
class ApiError(
message: String,
cause: Throwable? = null,
val response: Response? = null,
) : IOException(message, cause)
private fun Response.formatError(caption: String? = null) = when {
caption.isNullOrBlank() -> "HTTP $code $message ${request.method} ${request.url}"
else -> "$caption: HTTP $code $message ${request.method} ${request.url}"
}
private fun Request.formatError(ex: Throwable, caption: String? = null) = when {
caption.isNullOrBlank() -> "${ex.withCaption()} $method $url"
else -> "$caption: ${ex.withCaption()} $method $url"
}
/**
* 応答ボディのHTMLやテキストを整形する
*/
private fun simplifyErrorHtml(body: String): String {
// // JsonObjectとして解釈できるならエラーメッセージを検出する
// try {
// val json = body.decodeJsonObject()
// jsonErrorParser(json)?.notEmpty()?.let { return it }
// } catch (_: Throwable) {
// }
// // HTMLならタグの除去を試みる
// try {
// val ct = response.body?.contentType()
// if (ct?.subtype == "html") {
// // XXX HTMLデコードを省略
// return reWhiteSpace.replace(body," ").trim()
// }
// } catch (_: Throwable) {
// }
// XXX: Amazon S3 が403を返した場合にcontent-typeが?/xmlでserverがAmazonならXMLをパースしてエラーを整形することもできるが、多分必要ない
// 通常テキストの空白や改行を整理した文字列を返す
try {
return reWhiteSpace.replace(body, " ").trim()
} catch (_: Throwable) {
}
// 全部失敗したら入力そのまま
return body
}
/**
* エラー応答のステータス部分や本文を文字列にする
*/
fun parseErrorResponse(response: Response, body: String? = null): String =
try {
val request = response.request
StringBuilder().apply {
// 応答ボディのテキストがあれば追加
if (body.isNullOrBlank()) {
append("(missing response body)")
} else {
append(simplifyErrorHtml(body))
}
if (isNotEmpty()) append(' ')
append("(HTTP ").append(response.code.toString())
response.message.notBlank()?.let { message ->
append(' ')
append(message)
}
append(") ${request.method} ${request.url}")
}.toString().replace("""[\x0d\x0a]+""".toRegex(), "\n")
} catch (ex: Throwable) {
log.e(ex, "parseErrorResponse failed.")
"(can't parse response body)"
}
suspend fun Request.await(okHttp: OkHttpClient) =
try {
okHttp.newCall(this).await()
} catch (ex: Throwable) {
throw ApiError(cause = ex, message = this.formatError(ex))
}
/**
* レスポンスボディを文字列として読む
* ボディがない場合はnullを返す
* その他はSendExceptionを返す
*/
private suspend fun Response.readString(): String? {
val response = this
return try {
// XXX: 進捗表示
withContext(AppDispatchers.IO) {
val bodyString = response.body?.string()
if (bodyString.isNullOrEmpty()) {
if (response.code in 200 until 300) {
// Misskey の /api/notes/favorites/create は 204(no content)を返す。ボディはカラになる。
return@withContext ""
} else if (!response.isSuccessful) {
throw ApiError(
response = response,
message = parseErrorResponse(response = response, body = ""),
)
}
}
bodyString
}
} catch (ex: Throwable) {
when (ex) {
is CancellationException, is ApiError -> throw ex
else -> {
log.e(ex, "readString failed.")
throw ApiError(
response = response,
message = parseErrorResponse(
response = response,
ex.withCaption("readString failed.")
)
)
}
}
} finally {
response.body?.closeQuietly()
}
}
/**
* ResponseWith<String?> をResponseWith<JsonObject?>に変換する
*/
suspend fun String?.stringToJsonObject(response: Response): JsonObject =
try {
val content = this
withContext(AppDispatchers.IO) {
when {
content == null -> throw ApiError(
response = response,
message = response.formatError("response body is null.")
)
// 204 no content は 空オブジェクトと解釈する
content == "" -> JsonObject()
reStartJsonArray.containsMatchIn(content) ->
jsonObjectOf("root" to content.decodeJsonArray())
reStartJsonObject.containsMatchIn(content) -> {
val json = content.decodeJsonObject()
DEFAULT_JSON_ERROR_PARSER(json)?.let { error ->
throw ApiError(
response = response,
message = response.formatError(error)
)
}
json
}
else -> throw ApiError(
response = response,
message = response.formatError("not a JSON object.")
)
}
}
} catch (ex: Throwable) {
when (ex) {
is CancellationException, is ApiError -> throw ex
else -> {
throw ApiError(
response = response,
message = response.formatError("readJsonObject failed."),
cause = ex,
)
}
}
}
suspend fun Response.readJsonObject() = readString().stringToJsonObject(this)

View File

@ -6,7 +6,6 @@ import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.auth.AuthBase import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.pref.pref
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.*
import jp.juggler.util.data.* import jp.juggler.util.data.*
@ -56,7 +55,6 @@ class TootApiClient(
} }
// 認証に関する設定を保存する // 認証に関する設定を保存する
internal val pref = context.pref()
// インスタンスのホスト名 // インスタンスのホスト名
var apiHost: Host? = null var apiHost: Host? = null
@ -407,8 +405,9 @@ class TootApiClient(
requestBuilder.url(url) requestBuilder.url(url)
(forceAccessToken ?: account?.getAccessToken()) (forceAccessToken ?: account?.bearerAccessToken)?.notEmpty()?.let {
?.notEmpty()?.let { requestBuilder.header("Authorization", "Bearer $it") } requestBuilder.header("Authorization", "Bearer $it")
}
requestBuilder.build() requestBuilder.build()
.also { log.d("request: ${it.method} $url") } .also { log.d("request: ${it.method} $url") }
@ -503,10 +502,9 @@ class TootApiClient(
url = "$url${delm}i=${accessToken.encodePercent()}" url = "$url${delm}i=${accessToken.encodePercent()}"
} }
} else { } else {
val accessToken = account.getAccessToken() account.bearerAccessToken.notEmpty()?.let {
if (accessToken?.isNotEmpty() == true) { val delm = if (url.contains('?')) '&' else '?'
val delm = if (-1 != url.indexOf('?')) '&' else '?' url = "$url${delm}access_token=${it.encodePercent()}"
url = "$url${delm}access_token=${accessToken.encodePercent()}"
} }
} }

View File

@ -34,7 +34,7 @@ abstract class AuthBase {
val clientName val clientName
get() = arrayOf( get() = arrayOf(
testClientName, testClientName,
PrefS.spClientName.invoke(), PrefS.spClientName.value,
).firstNotNullOfOrNull { it.notBlank() } ).firstNotNullOfOrNull { it.notBlank() }
?: DEFAULT_CLIENT_NAME ?: DEFAULT_CLIENT_NAME

View File

@ -8,8 +8,8 @@ import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.InstanceType import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.table.ClientInfo import jp.juggler.subwaytooter.table.daoClientInfo
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonObject import jp.juggler.util.data.buildJsonObject
@ -84,7 +84,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
} }
} }
clientInfo[KEY_CLIENT_CREDENTIAL] = clientCredential clientInfo[KEY_CLIENT_CREDENTIAL] = clientCredential
ClientInfo.save(apiHost, clientName, clientInfo.toString()) daoClientInfo.save(apiHost, clientName, clientInfo.toString())
return clientCredential return clientCredential
} }
@ -95,7 +95,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
tootInstance: TootInstance?, tootInstance: TootInstance?,
forceUpdateClient: Boolean, forceUpdateClient: Boolean,
): JsonObject { ): JsonObject {
var clientInfo = ClientInfo.load(apiHost, clientName) var clientInfo = daoClientInfo.load(apiHost, clientName)
// スコープ一覧を取得する // スコープ一覧を取得する
val scopeString = mastodonScope(tootInstance) val scopeString = mastodonScope(tootInstance)
@ -126,7 +126,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
else -> try { else -> try {
// マストドン2.4でスコープが追加された // マストドン2.4でスコープが追加された
// 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない // 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない
ClientInfo.delete(apiHost, clientName) daoClientInfo.delete(apiHost, clientName)
// クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない // クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
// client credential だけは消せる // client credential だけは消せる
@ -254,7 +254,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
when { when {
param.startsWith("db:") -> try { param.startsWith("db:") -> try {
val dataId = param.substring(3).toLong(10) val dataId = param.substring(3).toLong(10)
val sa = SavedAccount.loadAccount(context, dataId) val sa = daoSavedAccount.loadAccount(dataId)
?: error("missing account db_id=$dataId") ?: error("missing account db_id=$dataId")
client.account = sa client.account = sa
} catch (ex: Throwable) { } catch (ex: Throwable) {
@ -272,7 +272,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
val apiHost = client.apiHost val apiHost = client.apiHost
?: error("can't get apiHost from callback parameter.") ?: error("can't get apiHost from callback parameter.")
val clientInfo = ClientInfo.load(apiHost, clientName) val clientInfo = daoClientInfo.load(apiHost, clientName)
?: error("can't find client info for apiHost=$apiHost, clientName=$clientName") ?: error("can't find client info for apiHost=$apiHost, clientName=$clientName")
val tokenInfo = api.authStep2( val tokenInfo = api.authStep2(

View File

@ -8,9 +8,9 @@ import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.EntityId import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefDevice import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.table.ClientInfo import jp.juggler.subwaytooter.table.daoClientInfo
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -96,14 +96,12 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
* {"token":"0ba88e2d-4b7d-4599-8d90-dc341a005637","url":"https://misskey.xyz/auth/0ba88e2d-4b7d-4599-8d90-dc341a005637"} * {"token":"0ba88e2d-4b7d-4599-8d90-dc341a005637","url":"https://misskey.xyz/auth/0ba88e2d-4b7d-4599-8d90-dc341a005637"}
*/ */
private suspend fun createAuthUri(apiHost: Host, appSecret: String): Uri { private suspend fun createAuthUri(apiHost: Host, appSecret: String): Uri {
PrefDevice.from(context).edit().apply { context.prefDevice.saveLastAuth(
putString(PrefDevice.LAST_AUTH_INSTANCE, apiHost.ascii) host = apiHost.ascii,
putString(PrefDevice.LAST_AUTH_SECRET, appSecret) secret = appSecret,
when (val account = account) { dbId = account?.db_id, //nullable
null -> remove(PrefDevice.LAST_AUTH_DB_ID) )
else -> putLong(PrefDevice.LAST_AUTH_DB_ID, account.db_id)
}
}.apply()
return api.authSessionGenerate(apiHost, appSecret) return api.authSessionGenerate(apiHost, appSecret)
.string("url").notEmpty()?.toUri() .string("url").notEmpty()?.toUri()
?: error("missing 'url' in session/generate.") ?: error("missing 'url' in session/generate.")
@ -122,7 +120,7 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
): Uri { ): Uri {
val apiHost = apiHost ?: error("missing apiHost") val apiHost = apiHost ?: error("missing apiHost")
val clientInfo = ClientInfo.load(apiHost, clientName) val clientInfo = daoClientInfo.load(apiHost, clientName)
// スコープ一覧を取得する // スコープ一覧を取得する
val scopeArray = getScopeArrayMisskey(ti) val scopeArray = getScopeArrayMisskey(ti)
@ -173,7 +171,7 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
val appSecret = appJson.string(KEY_MISSKEY_APP_SECRET) val appSecret = appJson.string(KEY_MISSKEY_APP_SECRET)
.notBlank() ?: error(context.getString(R.string.cant_get_misskey_app_secret)) .notBlank() ?: error(context.getString(R.string.cant_get_misskey_app_secret))
ClientInfo.save(apiHost, clientName, appJson.toString()) daoClientInfo.save(apiHost, clientName, appJson.toString())
return createAuthUri(apiHost, appSecret) return createAuthUri(apiHost, appSecret)
} }
@ -183,20 +181,21 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
*/ */
override suspend fun authStep2(uri: Uri): Auth2Result { override suspend fun authStep2(uri: Uri): Auth2Result {
val prefDevice = PrefDevice.from(context) val prefDevice = context.prefDevice
val token = uri.getQueryParameter("token") val token = uri.getQueryParameter("token")
?.notBlank() ?: error("missing token in callback URL") ?.notBlank() ?: error("missing token in callback URL")
val hostStr = prefDevice.getString(PrefDevice.LAST_AUTH_INSTANCE, null) val hostStr = prefDevice.lastAuthInstance
?.notBlank() ?: error("missing instance name.") ?.notBlank() ?: error("missing instance name.")
val apiHost = Host.parse(hostStr) val apiHost = Host.parse(hostStr)
when (val dbId = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID, -1L)) { when (val dbId = prefDevice.lastAuthDbId) {
// new registration // new registration
-1L -> client.apiHost = apiHost null -> client.apiHost = apiHost
// update access token // update access token
else -> SavedAccount.loadAccount(context, dbId)?.also { else -> daoSavedAccount.loadAccount(dbId)?.also {
client.account = it client.account = it
} ?: error("missing account db_id=$dbId") } ?: error("missing account db_id=$dbId")
} }
@ -209,7 +208,7 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
) )
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val clientInfo = ClientInfo.load(apiHost, clientName) val clientInfo = daoClientInfo.load(apiHost, clientName)
?.notEmpty() ?: error("missing client id") ?.notEmpty() ?: error("missing client id")
val appSecret = clientInfo.string(KEY_MISSKEY_APP_SECRET) val appSecret = clientInfo.string(KEY_MISSKEY_APP_SECRET)

View File

@ -7,8 +7,8 @@ import jp.juggler.subwaytooter.api.auth.MisskeyAuth10.Companion.encodeScopeArray
import jp.juggler.subwaytooter.api.auth.MisskeyAuth10.Companion.getScopeArrayMisskey import jp.juggler.subwaytooter.api.auth.MisskeyAuth10.Companion.getScopeArrayMisskey
import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefDevice import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
@ -60,14 +60,11 @@ class MisskeyAuth13(override val client: TootApiClient) : AuthBase() {
val sessionId = UUID.randomUUID().toString() val sessionId = UUID.randomUUID().toString()
PrefDevice.from(client.context).edit().apply { client.context.prefDevice.saveLastAuth(
putString(PrefDevice.LAST_AUTH_INSTANCE, apiHost.ascii) host = apiHost.ascii,
putString(PrefDevice.LAST_AUTH_SECRET, sessionId) secret = sessionId,
when (val account = account) { dbId = account?.db_id,
null -> remove(PrefDevice.LAST_AUTH_DB_ID) )
else -> putLong(PrefDevice.LAST_AUTH_DB_ID, account.db_id)
}
}.apply()
return api13.createAuthUrl( return api13.createAuthUrl(
apiHost = apiHost, apiHost = apiHost,
@ -82,20 +79,20 @@ class MisskeyAuth13(override val client: TootApiClient) : AuthBase() {
override suspend fun authStep2(uri: Uri): Auth2Result { override suspend fun authStep2(uri: Uri): Auth2Result {
// 認証開始時に保存した情報 // 認証開始時に保存した情報
val prefDevice = PrefDevice.from(client.context) val prefDevice = client.context.prefDevice
val savedSessionId = prefDevice.getString(PrefDevice.LAST_AUTH_SECRET, null) val savedSessionId = prefDevice.lastAuthSecret
val apiHost = prefDevice.getString(PrefDevice.LAST_AUTH_INSTANCE, null) val apiHost = prefDevice.lastAuthInstance
?.let { Host.parse(it) } ?.let { Host.parse(it) }
?: error("missing apiHost") ?: error("missing apiHost")
when (val dbId = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID, -1L)) { when (val dbId = prefDevice.lastAuthDbId) {
// new registration // new registration
-1L -> client.apiHost = apiHost null -> client.apiHost = apiHost
// update access token // update access token
else -> { else -> {
val sa = SavedAccount.loadAccount(context, dbId) val sa = daoSavedAccount.loadAccount(dbId)
?: error("missing account db_id=$dbId") ?: error("missing account db_id=$dbId")
client.account = sa client.account = sa
} }
@ -131,7 +128,7 @@ class MisskeyAuth13(override val client: TootApiClient) : AuthBase() {
.account(accountJson) .account(accountJson)
?: error("can't parse user json.") ?: error("can't parse user json.")
prefDevice.edit().remove(PrefDevice.LAST_AUTH_SECRET).apply() prefDevice.removeLastAuth()
return Auth2Result( return Auth2Result(
tootInstance = ti, tootInstance = ti,

View File

@ -29,7 +29,7 @@ class EntityId(val x: String) : Comparable<EntityId> {
fun mayNull(x: String?) = if (x == null) null else EntityId(x) fun mayNull(x: String?) = if (x == null) null else EntityId(x)
fun String.decode(): EntityId? { fun String.decodeEntityId(): EntityId? {
if (this.isEmpty()) return null if (this.isEmpty()) return null
// first character is 'L' for EntityIdLong, 'S' for EntityIdString. // first character is 'L' for EntityIdLong, 'S' for EntityIdString.
// integer id is removed at https://source.joinmastodon.org/mastodon/docs/commit/e086d478afa140e7b0b9a60183655315966ad9ff // integer id is removed at https://source.joinmastodon.org/mastodon/docs/commit/e086d478afa140e7b0b9a60183655315966ad9ff
@ -37,26 +37,24 @@ class EntityId(val x: String) : Comparable<EntityId> {
} }
fun from(intent: Intent?, key: String) = fun from(intent: Intent?, key: String) =
intent?.string(key)?.decode() intent?.string(key)?.decodeEntityId()
fun from(bundle: Bundle?, key: String) = fun from(bundle: Bundle?, key: String) =
bundle?.string(key)?.decode() bundle?.string(key)?.decodeEntityId()
// 内部保存データのデコード用。APIレスポンスのパースに使ってはいけない // 内部保存データのデコード用。APIレスポンスのパースに使ってはいけない
fun from(data: JsonObject?, key: String): EntityId? { fun from(data: JsonObject?, key: String): EntityId? {
val o = data?.get(key) val o = data?.get(key)
if (o is Long) return EntityId(o.toString()) if (o is Long) return EntityId(o.toString())
return (o as? String)?.decode() return (o as? String)?.decodeEntityId()
} }
fun from(cursor: Cursor, key: String) = fun from(cursor: Cursor, key: String) =
cursor.getStringOrNull(key)?.decode() cursor.getStringOrNull(key)?.decodeEntityId()
} }
private fun encode(): String { // 昔は文字列とLong値を区別していたが、今はもうない
val prefix = 'S' fun encode(): String = "S$this"
return "$prefix$this"
}
fun putTo(data: Intent, key: String): Intent = data.putExtra(key, encode()) fun putTo(data: Intent, key: String): Intent = data.putExtra(key, encode())

View File

@ -11,6 +11,7 @@ import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
@ -215,7 +216,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
this.fields = parseMisskeyFields(src) this.fields = parseMisskeyFields(src)
UserRelation.fromAccount(parser, src, id) daoUserRelation.fromAccount(parser, src, id)
@Suppress("LeakingThis") @Suppress("LeakingThis")
MisskeyAccountDetailMap.fromAccount(parser, this, id) MisskeyAccountDetailMap.fromAccount(parser, this, id)
@ -504,7 +505,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
.append(suggestionSource) .append(suggestionSource)
} }
if (PrefB.bpDirectoryLastActive() && last_status_at > 0L) { if (PrefB.bpDirectoryLastActive.value && last_status_at > 0L) {
prepareSb() prepareSb()
.append(context.getString(R.string.last_active)) .append(context.getString(R.string.last_active))
.append(delm) .append(delm)
@ -519,7 +520,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
} }
if (!fromProfileHeader) { if (!fromProfileHeader) {
if (PrefB.bpDirectoryTootCount() && if (PrefB.bpDirectoryTootCount.value &&
(statuses_count ?: 0L) > 0L (statuses_count ?: 0L) > 0L
) { ) {
prepareSb() prepareSb()
@ -528,8 +529,8 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
.append(statuses_count.toString()) .append(statuses_count.toString())
} }
if (PrefB.bpDirectoryFollowers() && if (PrefB.bpDirectoryFollowers.value &&
!PrefB.bpHideFollowCount() && !PrefB.bpHideFollowCount.value &&
(followers_count ?: 0L) > 0L (followers_count ?: 0L) > 0L
) { ) {
prepareSb() prepareSb()
@ -538,7 +539,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
.append(followers_count.toString()) .append(followers_count.toString())
} }
if (PrefB.bpDirectoryNote() && note?.isNotEmpty() == true) { if (PrefB.bpDirectoryNote.value && note?.isNotEmpty() == true) {
val decodedNote = DecodeOptions( val decodedNote = DecodeOptions(
context, context,
accessInfo, accessInfo,

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.api.entity package jp.juggler.subwaytooter.api.entity
import android.content.SharedPreferences
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.util.* import jp.juggler.util.*
@ -191,26 +190,26 @@ class TootAttachment : TootAttachmentLike {
private fun parseType(src: String?) = private fun parseType(src: String?) =
TootAttachmentType.values().find { it.id == src } TootAttachmentType.values().find { it.id == src }
override fun urlForThumbnail(pref: SharedPreferences) = override fun urlForThumbnail() =
if (PrefB.bpPriorLocalURL(pref)) { if (PrefB.bpPriorLocalURL.value) {
preview_url.notEmpty() ?: preview_remote_url.notEmpty() preview_url.notEmpty() ?: preview_remote_url.notEmpty()
} else { } else {
preview_remote_url.notEmpty() ?: preview_url.notEmpty() preview_remote_url.notEmpty() ?: preview_url.notEmpty()
} ?: when (type) { } ?: when (type) {
TootAttachmentType.Image -> getLargeUrl(pref) TootAttachmentType.Image -> getLargeUrl()
else -> null else -> null
} }
fun getLargeUrl(pref: SharedPreferences) = fun getLargeUrl() =
if (PrefB.bpPriorLocalURL(pref)) { if (PrefB.bpPriorLocalURL.value) {
url.notEmpty() ?: remote_url url.notEmpty() ?: remote_url
} else { } else {
remote_url.notEmpty() ?: url remote_url.notEmpty() ?: url
} }
fun getLargeUrlList(pref: SharedPreferences) = fun getLargeUrlList() =
ArrayList<String>().apply { ArrayList<String>().apply {
if (PrefB.bpPriorLocalURL(pref)) { if (PrefB.bpPriorLocalURL.value) {
url.notEmpty()?.addTo(this) url.notEmpty()?.addTo(this)
remote_url.notEmpty()?.addTo(this) remote_url.notEmpty()?.addTo(this)
} else { } else {

View File

@ -1,7 +1,5 @@
package jp.juggler.subwaytooter.api.entity package jp.juggler.subwaytooter.api.entity
import android.content.SharedPreferences
enum class TootAttachmentType(val id: String) { enum class TootAttachmentType(val id: String) {
Unknown("unknown"), Unknown("unknown"),
Image("image"), Image("image"),
@ -16,7 +14,7 @@ interface TootAttachmentLike {
val description: String? val description: String?
// url for thumbnail, or null or empty // url for thumbnail, or null or empty
fun urlForThumbnail(pref: SharedPreferences): String? fun urlForThumbnail(): String?
// url for description, or null or empty // url for description, or null or empty
val urlForDescription: String? val urlForDescription: String?

View File

@ -14,7 +14,7 @@ class TootAttachmentMSP(
override val description: String? override val description: String?
get() = null get() = null
override fun urlForThumbnail(pref: SharedPreferences) = preview_url override fun urlForThumbnail() = preview_url
override val urlForDescription: String override val urlForDescription: String
get() = preview_url get() = preview_url

View File

@ -1,7 +1,6 @@
package jp.juggler.subwaytooter.api.entity package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.pref.pref
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.filterNotEmpty import jp.juggler.util.data.filterNotEmpty
@ -61,7 +60,7 @@ class TootCard(
}, },
image = src.media_attachments image = src.media_attachments
?.firstOrNull() ?.firstOrNull()
?.urlForThumbnail(parser.context.pref()) ?.urlForThumbnail()
?: src.account.avatar_static, ?: src.account.avatar_static,
type = "photo" type = "photo"
) )

View File

@ -310,8 +310,9 @@ class TootInstance(parser: TootParser, src: JsonObject) {
if (sendRequest(result) { if (sendRequest(result) {
val builder = Request.Builder().url("https://${apiHost?.ascii}/api/v1/instance") val builder = Request.Builder().url("https://${apiHost?.ascii}/api/v1/instance")
(forceAccessToken ?: account?.getAccessToken()) (forceAccessToken ?: account?.bearerAccessToken)?.notEmpty()?.let {
?.notEmpty()?.let { builder.header("Authorization", "Bearer $it") } builder.header("Authorization", "Bearer $it")
}
builder.build() builder.build()
} }
) { ) {
@ -428,7 +429,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
when { when {
qrr.first?.instanceType == InstanceType.Pixelfed && qrr.first?.instanceType == InstanceType.Pixelfed &&
!PrefB.bpEnablePixelfed() && !PrefB.bpEnablePixelfed.value &&
!req.allowPixelfed -> !req.allowPixelfed ->
tiError("currently Pixelfed instance is not supported.") tiError("currently Pixelfed instance is not supported.")
else -> qrr else -> qrr

View File

@ -9,7 +9,6 @@ import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.EmojiDecoder import jp.juggler.subwaytooter.util.EmojiDecoder
import jp.juggler.util.*
import jp.juggler.util.data.* import jp.juggler.util.data.*
import java.util.* import java.util.*
@ -162,7 +161,7 @@ class TootReaction(
} }
private fun chooseUrl() = when { private fun chooseUrl() = when {
PrefB.bpDisableEmojiAnimation() -> staticUrl PrefB.bpDisableEmojiAnimation.value -> staticUrl
else -> url else -> url
} }

View File

@ -5,7 +5,6 @@ import android.content.Context
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import androidx.annotation.StringRes import androidx.annotation.StringRes
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootAccountMap import jp.juggler.subwaytooter.api.TootAccountMap
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser
@ -365,7 +364,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
this.mentions = mergeMentions(mentions1, mentions2) this.mentions = mergeMentions(mentions1, mentions2)
this.decoded_mentions = this.decoded_mentions =
HTMLDecoder.decodeMentions(parser.linkHelper, this) HTMLDecoder.decodeMentions(parser, this)
?: EMPTY_SPANNABLE ?: EMPTY_SPANNABLE
// contentを読んだ後にアンケートのデコード // contentを読んだ後にアンケートのデコード
@ -484,7 +483,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
this.muted = false this.muted = false
this.language = null this.language = null
this.decoded_mentions = this.decoded_mentions =
HTMLDecoder.decodeMentions(parser.linkHelper, this) HTMLDecoder.decodeMentions(parser, this)
?: EMPTY_SPANNABLE ?: EMPTY_SPANNABLE
val quote = when { val quote = when {
@ -696,7 +695,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
this.muted = src.optBoolean("muted") this.muted = src.optBoolean("muted")
this.language = src.string("language")?.notEmpty() this.language = src.string("language")?.notEmpty()
this.decoded_mentions = this.decoded_mentions =
HTMLDecoder.decodeMentions(parser.linkHelper, this) HTMLDecoder.decodeMentions(parser, this)
?: EMPTY_SPANNABLE ?: EMPTY_SPANNABLE
val quote = when { val quote = when {
@ -1100,7 +1099,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
fun markDeleted(context: Context, deletedAt: Long?): Boolean { fun markDeleted(context: Context, deletedAt: Long?): Boolean {
if (PrefB.bpDontRemoveDeletedToot(App1.getAppState(context).pref)) return false if (PrefB.bpDontRemoveDeletedToot.value) return false
var sv = if (deletedAt != null) { var sv = if (deletedAt != null) {
context.getString(R.string.status_deleted_at, formatTime(context, deletedAt, false)) context.getString(R.string.status_deleted_at, formatTime(context, deletedAt, false))
@ -1131,7 +1130,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
internal val log = LogCategory("TootStatus") internal val log = LogCategory("TootStatus")
@Volatile @Volatile
internal var muted_app: HashSet<String>? = null internal var muted_app: Set<String>? = null
@Volatile @Volatile
internal var muted_word: WordTrieTree? = null internal var muted_word: WordTrieTree? = null
@ -1419,7 +1418,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
formatDate(t, date_format2, omitZeroSecond = false, omitYear = true) formatDate(t, date_format2, omitZeroSecond = false, omitYear = true)
} }
if (bAllowRelative && PrefB.bpRelativeTimestamp()) { if (bAllowRelative && PrefB.bpRelativeTimestamp.value) {
delta = abs(delta) delta = abs(delta)

View File

@ -0,0 +1,67 @@
package jp.juggler.subwaytooter.api.push
import jp.juggler.subwaytooter.api.await
import jp.juggler.subwaytooter.api.readJsonObject
import jp.juggler.subwaytooter.column.encodeQuery
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.data.jsonArrayOf
import jp.juggler.util.network.toPostRequestBuilder
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
/**
* アプリサーバのAPI
*/
class ApiPushAppServer(
private val okHttp: OkHttpClient,
private val appServerPrefix: String = "https://mastodon-msg.juggler.jp/api/v2",
) {
/**
* 中継エンドポイントが無効になったら削除する
*/
suspend fun endpointRemove(
upUrl: String? = null,
fcmToken: String? = null,
hashId:String? = null,
): JsonObject = buildJsonObject {
upUrl?.let { put("upUrl", it) }
fcmToken?.let { put("fcmToken", it) }
hashId?.let{ put("hashId", it) }
}.encodeQuery().let {
Request.Builder()
.url("${appServerPrefix}/endpoint/remove?$it")
}.delete().build()
.await(okHttp)
.readJsonObject()
/**
* エンドポイントとアカウントハッシュをアプリサーバに登録する
*/
suspend fun endpointUpsert(
upUrl: String?,
fcmToken: String?,
acctHashList: List<String>,
): JsonObject =
buildJsonObject {
upUrl?.let { put("upUrl", it) }
fcmToken?.let { put("fcmToken", it) }
put("acctHashList", jsonArrayOf(*(acctHashList.toTypedArray())))
}.toPostRequestBuilder()
.url("${appServerPrefix}/endpoint/upsert")
.build()
.await(okHttp)
.readJsonObject()
suspend fun getLargeObject(
largeObjectId: String
): ByteArray? = withContext(AppDispatchers.IO) {
Request.Builder()
.url("${appServerPrefix}/l/$largeObjectId")
.build()
.await(okHttp)
.body?.bytes()
}
}

View File

@ -0,0 +1,118 @@
package jp.juggler.subwaytooter.api.push
import jp.juggler.subwaytooter.api.authorizationBearer
import jp.juggler.subwaytooter.api.await
import jp.juggler.subwaytooter.api.readJsonObject
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.network.toPutRequestBuilder
import okhttp3.OkHttpClient
import okhttp3.Request
class ApiPushMastodon(
private val okHttp: OkHttpClient,
) {
companion object {
val alertTypes = arrayOf(
"mention",
"status",
"reblog",
"follow",
"follow_request",
"favourite",
"poll",
"update",
"admin.sign_up",
"admin.report",
)
}
/**
* アクセストークンに設定されたプッシュ購読を見る
*/
suspend fun getPushSubscription(
a: SavedAccount,
): JsonObject = Request.Builder()
.url("https://${a.apiHost}/api/v1/push/subscription")
.authorizationBearer(a.bearerAccessToken)
.build()
.await(okHttp)
.readJsonObject()
/**
* アクセストークンに設定されたプッシュ購読を削除する
*/
suspend fun deletePushSubscription(
a: SavedAccount,
): JsonObject = Request.Builder()
.delete()
.url("https://${a.apiHost}/api/v1/push/subscription")
.authorizationBearer(a.bearerAccessToken)
.build()
.await(okHttp)
.readJsonObject()
/**
* アクセストークンに対してプッシュ購読を登録する
*/
suspend fun createPushSubscription(
a: SavedAccount,
// REQUIRED String. The endpoint URL that is called when a notification event occurs.
endpointUrl: String,
// REQUIRED String. User agent public key.
// Base64 encoded string of a public key from a ECDH keypair using the prime256v1 curve.
p256dh: String,
// REQUIRED String. Auth secret. Base64 encoded string of 16 bytes of random data.
auth: String,
// map of alert type to boolean, true to receive for alert type. false? null?
alerts: Map<String, Boolean>,
// whether to receive push notifications from all, followed, follower, or none users.
policy: String,
): JsonObject = buildJsonObject {
put("subscription", buildJsonObject {
put("endpoint", endpointUrl)
put("keys", buildJsonObject {
put("p256dh", p256dh)
put("auth", auth)
})
})
put("data", buildJsonObject {
put("alerts", buildJsonObject {
for (t in alertTypes) {
alerts[t]?.let { put(t, it) }
}
})
})
put("policy", policy)
}.toPostRequestBuilder()
.url("https://${a.apiHost}/api/v1/push/subscription")
.authorizationBearer(a.bearerAccessToken)
.build()
.await(okHttp)
.readJsonObject()
/**
* 購読のdata部分を更新する
*/
suspend fun updatePushSubscriptionData(
a: SavedAccount,
alerts: Map<String, Boolean>,
policy: String,
): JsonObject = buildJsonObject {
put("data", buildJsonObject {
put("alerts", buildJsonObject {
for (t in alertTypes) {
alerts[t]?.let { put(t, it) }
}
})
})
put("policy", policy)
}.toPutRequestBuilder()
.url("https://${a.apiHost}/api/v1/push/subscription")
.authorizationBearer(a.bearerAccessToken)
.build()
.await(okHttp)
.readJsonObject()
}

View File

@ -0,0 +1,77 @@
package jp.juggler.subwaytooter.api.push
import jp.juggler.subwaytooter.api.await
import jp.juggler.subwaytooter.api.readJsonObject
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.network.toPostRequestBuilder
import okhttp3.OkHttpClient
class ApiPushMisskey(
private val okHttp: OkHttpClient,
) {
/**
* エンドポイントURLを指定してプッシュ購読の情報を取得する
*/
suspend fun getPushSubscription(
a: SavedAccount,
endpoint: String,
): JsonObject = buildJsonObject {
a.misskeyApiToken?.let { put("i", it) }
put("endpoint", endpoint)
}.toPostRequestBuilder()
.url("https://${a.apiHost}/api/sw/show-registration")
.build()
.await(okHttp)
.readJsonObject()
suspend fun deletePushSubscription(
a: SavedAccount,
endpoint: String,
): JsonObject = buildJsonObject {
a.misskeyApiToken?.let { put("i", it) }
put("endpoint", endpoint)
}.toPostRequestBuilder()
.url("https://${a.apiHost}/api/sw/unregister")
.build()
.await(okHttp)
.readJsonObject()
/**
* プッシュ購読を更新する
* endpointのURLはクエリに使われる変更できるのはsendReadMessageだけ
*/
suspend fun updatePushSubscription(
a: SavedAccount,
endpoint: String,
sendReadMessage: Boolean,
): JsonObject = buildJsonObject {
a.misskeyApiToken?.let { put("i", it) }
put("endpoint", endpoint)
put("sendReadMessage", sendReadMessage)
}.toPostRequestBuilder()
.url("https://${a.apiHost}/api/sw/update-registration")
.build()
.await(okHttp)
.readJsonObject()
suspend fun createPushSubscription(
a: SavedAccount,
endpoint: String,
auth: String,
publicKey: String,
sendReadMessage: Boolean,
): JsonObject = buildJsonObject {
a.misskeyApiToken?.let { put("i", it) }
put("endpoint", endpoint)
put("auth", auth)
put("publickey", publicKey)
put("sendReadMessage", sendReadMessage)
}.toPostRequestBuilder()
.url("https://${a.apiHost}/api/sw/register")
.build()
.await(okHttp)
.readJsonObject()
}

View File

@ -5,7 +5,6 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.database.Cursor import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.net.Uri import android.net.Uri
import android.provider.BaseColumns import android.provider.BaseColumns
import android.util.JsonReader import android.util.JsonReader
@ -17,10 +16,9 @@ import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.column.Column import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.ColumnEncoder import jp.juggler.subwaytooter.column.ColumnEncoder
import jp.juggler.subwaytooter.column.getBackgroundImageDir import jp.juggler.subwaytooter.column.getBackgroundImageDir
import jp.juggler.subwaytooter.global.appDatabase
import jp.juggler.subwaytooter.pref.PrefL import jp.juggler.subwaytooter.pref.PrefL
import jp.juggler.subwaytooter.pref.impl.* import jp.juggler.subwaytooter.pref.impl.*
import jp.juggler.subwaytooter.pref.put import jp.juggler.subwaytooter.pref.lazyPref
import jp.juggler.subwaytooter.table.* import jp.juggler.subwaytooter.table.*
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.data.* import jp.juggler.util.data.*
@ -117,50 +115,49 @@ object AppDataExporter {
writer.name(jsonKey) writer.name(jsonKey)
writer.beginArray() writer.beginArray()
appDatabase.query(table, null, null, null, null, null, null) appDatabase.rawQuery("select from $table", emptyArray()).use { cursor ->
?.use { cursor -> val names = ArrayList<String>()
val names = ArrayList<String>() val column_count = cursor.columnCount
val column_count = cursor.columnCount for (i in 0 until column_count) {
for (i in 0 until column_count) { names.add(cursor.getColumnName(i))
names.add(cursor.getColumnName(i))
}
while (cursor.moveToNext()) {
writer.beginObject()
for (i in 0 until column_count) {
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_NULL -> {
writer.name(names[i])
writer.nullValue()
}
Cursor.FIELD_TYPE_INTEGER -> {
writer.name(names[i])
writer.value(cursor.getLong(i))
}
Cursor.FIELD_TYPE_STRING -> {
writer.name(names[i])
writer.value(cursor.getString(i))
}
Cursor.FIELD_TYPE_FLOAT -> {
val d = cursor.getDouble(i)
if (d.isNaN() || d.isInfinite()) {
log.w("column ${names[i]} is nan or infinite value.")
} else {
writer.name(names[i])
writer.value(d)
}
}
Cursor.FIELD_TYPE_BLOB -> log.w("column ${names[i]} is blob.")
}
}
writer.endObject()
}
} }
while (cursor.moveToNext()) {
writer.beginObject()
for (i in 0 until column_count) {
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_NULL -> {
writer.name(names[i])
writer.nullValue()
}
Cursor.FIELD_TYPE_INTEGER -> {
writer.name(names[i])
writer.value(cursor.getLong(i))
}
Cursor.FIELD_TYPE_STRING -> {
writer.name(names[i])
writer.value(cursor.getString(i))
}
Cursor.FIELD_TYPE_FLOAT -> {
val d = cursor.getDouble(i)
if (d.isNaN() || d.isInfinite()) {
log.w("column ${names[i]} is nan or infinite value.")
} else {
writer.name(names[i])
writer.value(d)
}
}
Cursor.FIELD_TYPE_BLOB -> log.w("column ${names[i]} is blob.")
}
}
writer.endObject()
}
}
writer.endArray() writer.endArray()
} }
@ -198,19 +195,21 @@ object AppDataExporter {
} }
if (SavedAccount.table == table) { if (SavedAccount.table == table) {
// 一時的に存在したが現在のDBスキーマにはない項目は読み飛ばす when (name) {
if ("nickname" == name || "color" == name) { // 一時的に存在したが現在のDBスキーマにはない項目は読み飛ばす
reader.skipValue() "nickname",
continue "color",
} "notification_server",
"register_key",
// リアルタイム通知に関連する項目は読み飛ばす "register_time",
if (SavedAccount.COL_NOTIFICATION_TAG.name == name || "last_notification_error",
SavedAccount.COL_REGISTER_KEY.name == name || "last_subscription_error",
SavedAccount.COL_REGISTER_TIME.name == name "last_push_endpoint",
) { -> {
reader.skipValue() reader.skipValue()
continue continue
}
else -> Unit
} }
} }
@ -230,8 +229,7 @@ object AppDataExporter {
} }
} }
reader.endObject() reader.endObject()
val new_id = val new_id = db.replace(table, null, cv)
db.insertWithOnConflict(table, null, cv, SQLiteDatabase.CONFLICT_REPLACE)
if (new_id == -1L) error("importTable: invalid row_id") if (new_id == -1L) error("importTable: invalid row_id")
idMap?.put(old_id, new_id) idMap?.put(old_id, new_id)
} }
@ -359,7 +357,7 @@ object AppDataExporter {
val app_state = App1.getAppState(context) val app_state = App1.getAppState(context)
writePref(writer, app_state.pref) writePref(writer, lazyPref)
writeFromTable(writer, KEY_ACCOUNT, SavedAccount.table) writeFromTable(writer, KEY_ACCOUNT, SavedAccount.table)
writeFromTable(writer, KEY_ACCT_COLOR, AcctColor.table) writeFromTable(writer, KEY_ACCT_COLOR, AcctColor.table)
@ -386,12 +384,12 @@ object AppDataExporter {
while (reader.hasNext()) { while (reader.hasNext()) {
when (reader.nextName()) { when (reader.nextName()) {
KEY_PREF -> importPref(reader, app_state.pref) KEY_PREF -> importPref(reader, lazyPref)
KEY_ACCOUNT -> importTable(reader, SavedAccount.table, account_id_map) KEY_ACCOUNT -> importTable(reader, SavedAccount.table, account_id_map)
KEY_ACCT_COLOR -> { KEY_ACCT_COLOR -> {
importTable(reader, AcctColor.table, null) importTable(reader, AcctColor.table, null)
AcctColor.clearMemoryCache() daoAcctColor.clearMemoryCache()
} }
KEY_MUTED_APP -> importTable(reader, MutedApp.table, null) KEY_MUTED_APP -> importTable(reader, MutedApp.table, null)
@ -408,10 +406,10 @@ object AppDataExporter {
} }
run { run {
val old_id = PrefL.lpTabletTootDefaultAccount(app_state.pref) val old_id = PrefL.lpTabletTootDefaultAccount.value
if (old_id != -1L) { if (old_id != -1L) {
val new_id = account_id_map[old_id] val new_id = account_id_map[old_id]
app_state.pref.edit().put(PrefL.lpTabletTootDefaultAccount, new_id ?: -1L).apply() PrefL.lpTabletTootDefaultAccount.value = new_id ?: -1L
} }
} }

View File

@ -10,12 +10,17 @@ import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.actmain.selectPushDistributor
import jp.juggler.subwaytooter.dialog.runInProgress
import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable
import jp.juggler.subwaytooter.itemviewholder.AdditionalButtonsPosition import jp.juggler.subwaytooter.itemviewholder.AdditionalButtonsPosition
import jp.juggler.subwaytooter.pref.* import jp.juggler.subwaytooter.pref.*
import jp.juggler.subwaytooter.pref.impl.* import jp.juggler.subwaytooter.pref.impl.*
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.table.sortedByNickname
import jp.juggler.subwaytooter.util.CustomShareTarget import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.openBrowser import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast import jp.juggler.util.data.cast
import jp.juggler.util.data.intentOpenDocument import jp.juggler.util.data.intentOpenDocument
import jp.juggler.util.data.notZero import jp.juggler.util.data.notZero
@ -24,6 +29,7 @@ import jp.juggler.util.ui.InputTypeEx
import jp.juggler.util.ui.attrColor import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.getAdaptiveRippleDrawable import jp.juggler.util.ui.getAdaptiveRippleDrawable
import jp.juggler.util.ui.getAdaptiveRippleDrawableRound import jp.juggler.util.ui.getAdaptiveRippleDrawableRound
import kotlinx.coroutines.delay
import org.jetbrains.anko.backgroundDrawable import org.jetbrains.anko.backgroundDrawable
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -227,6 +233,10 @@ class AppSettingItem(
val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_setting).apply { val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_setting).apply {
section(R.string.notifications) { section(R.string.notifications) {
action(R.string.push_distributor) {
action = { selectPushDistributor() }
desc = R.string.push_distributor_desc
}
text( text(
PrefS.spPullNotificationCheckInterval, PrefS.spPullNotificationCheckInterval,
@ -447,14 +457,18 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
) { ) {
val lp = pref.cast<LongPref>()!! val lp = pref.cast<LongPref>()!!
spinnerInitializer = { spinner -> spinnerInitializer = { spinner ->
val adapter = AccountAdapter() launchAndShowError {
spinner.adapter = adapter val list = daoSavedAccount.loadAccountList()
spinner.setSelection(adapter.getIndexFromId(lp(pref))) .sortedByNickname()
val adapter = AccountAdapter(list)
spinner.adapter = adapter
spinner.setSelection(adapter.getIndexFromId(lp.value))
}
} }
spinnerOnSelected = { spinner, index -> spinnerOnSelected = { spinner, index ->
val adapter = spinner.adapter.cast<ActAppSetting.AccountAdapter>() spinner.adapter.cast<ActAppSetting.AccountAdapter>()
?: error("spinnerOnSelected: missing AccountAdapter") ?.getIdFromIndex(index)
pref.edit().put(lp, adapter.getIdFromIndex(index)).apply() ?.let { lp.value = it }
} }
} }
@ -522,12 +536,12 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
spinnerInitializer = { spinner -> spinnerInitializer = { spinner ->
val adapter = TimeZoneAdapter() val adapter = TimeZoneAdapter()
spinner.adapter = adapter spinner.adapter = adapter
spinner.setSelection(adapter.getIndexFromId(sp(pref))) spinner.setSelection(adapter.getIndexFromId(sp.value))
} }
spinnerOnSelected = { spinner, index -> spinnerOnSelected = { spinner, index ->
val adapter = spinner.adapter.cast<ActAppSetting.TimeZoneAdapter>() val adapter = spinner.adapter.cast<ActAppSetting.TimeZoneAdapter>()
?: error("spinnerOnSelected: missing TimeZoneAdapter") ?: error("spinnerOnSelected: missing TimeZoneAdapter")
pref.edit().put(sp, adapter.getIdFromIndex(index)).apply() sp.value = adapter.getIdFromIndex(index)
} }
} }
@ -581,7 +595,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
} }
} }
onClickReset = { onClickReset = {
pref.edit().remove(item.pref?.key).apply() item.pref?.removeValue()
showTimelineFont(item) showTimelineFont(item)
} }
showTextView = { showTimelineFont(item, it) } showTextView = { showTimelineFont(item, it) }
@ -602,7 +616,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
} }
} }
onClickReset = { onClickReset = {
pref.edit().remove(item.pref?.key).apply() item.pref?.removeValue()
showTimelineFont(AppSettingItem.TIMELINE_FONT_BOLD) showTimelineFont(AppSettingItem.TIMELINE_FONT_BOLD)
} }
showTextView = { showTimelineFont(item, it) } showTextView = { showTimelineFont(item, it) }
@ -621,7 +635,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
fromFloat = { formatFontSize(it) } fromFloat = { formatFontSize(it) }
captionFontSize = { captionFontSize = {
val fv = fp(pref) val fv = fp.value
when { when {
!fv.isFinite() -> PrefF.default_timeline_font_size !fv.isFinite() -> PrefF.default_timeline_font_size
fv < 1f -> 1f fv < 1f -> 1f
@ -629,7 +643,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
} }
} }
captionSpacing = { captionSpacing = {
PrefS.spTimelineSpacing(pref).toFloatOrNull() PrefS.spTimelineSpacing.value.toFloatOrNull()
} }
changed = { changed = {
findItemViewHolder(item)?.updateCaption() findItemViewHolder(item)?.updateCaption()
@ -644,7 +658,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
fromFloat = { formatFontSize(it) } fromFloat = { formatFontSize(it) }
captionFontSize = { captionFontSize = {
val fv = fp(pref) val fv = fp.value
when { when {
!fv.isFinite() -> PrefF.default_acct_font_size !fv.isFinite() -> PrefF.default_acct_font_size
fv < 1f -> 1f fv < 1f -> 1f
@ -667,7 +681,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
fromFloat = { formatFontSize(it) } fromFloat = { formatFontSize(it) }
captionFontSize = { captionFontSize = {
val fv = fp(pref) val fv = fp.value
when { when {
!fv.isFinite() -> PrefF.default_notification_tl_font_size !fv.isFinite() -> PrefF.default_notification_tl_font_size
fv < 1f -> 1f fv < 1f -> 1f
@ -675,7 +689,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
} }
} }
captionSpacing = { captionSpacing = {
PrefS.spTimelineSpacing(pref).toFloatOrNull() PrefS.spTimelineSpacing.value.toFloatOrNull()
} }
changed = { changed = {
findItemViewHolder(item)?.updateCaption() findItemViewHolder(item)?.updateCaption()
@ -718,7 +732,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
fromFloat = { formatFontSize(it) } fromFloat = { formatFontSize(it) }
captionFontSize = { captionFontSize = {
val fv = fp(pref) val fv = fp.value
when { when {
!fv.isFinite() -> PrefF.default_header_font_size !fv.isFinite() -> PrefF.default_header_font_size
fv < 1f -> 1f fv < 1f -> 1f
@ -843,8 +857,8 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
val ivColumnHeader: ImageView = viewRoot.findViewById(R.id.ivColumnHeader) val ivColumnHeader: ImageView = viewRoot.findViewById(R.id.ivColumnHeader)
val tvColumnName: TextView = viewRoot.findViewById(R.id.tvColumnName) val tvColumnName: TextView = viewRoot.findViewById(R.id.tvColumnName)
val colorColumnHeaderBg = PrefI.ipCcdHeaderBg(activity.pref) val colorColumnHeaderBg = PrefI.ipCcdHeaderBg.value
val colorColumnHeaderFg = PrefI.ipCcdHeaderFg(activity.pref) val colorColumnHeaderFg = PrefI.ipCcdHeaderFg.value
val headerBg = when { val headerBg = when {
colorColumnHeaderBg != 0 -> colorColumnHeaderBg colorColumnHeaderBg != 0 -> colorColumnHeaderBg
@ -876,9 +890,9 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
val tvSampleAcct: TextView = viewRoot.findViewById(R.id.tvSampleAcct) val tvSampleAcct: TextView = viewRoot.findViewById(R.id.tvSampleAcct)
val tvSampleContent: TextView = viewRoot.findViewById(R.id.tvSampleContent) val tvSampleContent: TextView = viewRoot.findViewById(R.id.tvSampleContent)
val colorColumnBg = PrefI.ipCcdContentBg(activity.pref) val colorColumnBg = PrefI.ipCcdContentBg.value
val colorColumnAcct = PrefI.ipCcdContentAcct(activity.pref) val colorColumnAcct = PrefI.ipCcdContentAcct.value
val colorColumnText = PrefI.ipCcdContentText(activity.pref) val colorColumnText = PrefI.ipCcdContentText.value
flColumnBackground.setBackgroundColor(colorColumnBg) // may 0 flColumnBackground.setBackgroundColor(colorColumnBg) // may 0
@ -909,7 +923,6 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
group(R.string.footer_color) { group(R.string.footer_color) {
AppSettingItem.SAMPLE_FOOTER = AppSettingItem.SAMPLE_FOOTER =
sample(R.layout.setting_sample_footer) { activity, viewRoot -> sample(R.layout.setting_sample_footer) { activity, viewRoot ->
val pref = activity.pref
val ivFooterToot: AppCompatImageView = viewRoot.findViewById(R.id.ivFooterToot) val ivFooterToot: AppCompatImageView = viewRoot.findViewById(R.id.ivFooterToot)
val ivFooterMenu: AppCompatImageView = viewRoot.findViewById(R.id.ivFooterMenu) val ivFooterMenu: AppCompatImageView = viewRoot.findViewById(R.id.ivFooterMenu)
val llFooterBG: View = viewRoot.findViewById(R.id.llFooterBG) val llFooterBG: View = viewRoot.findViewById(R.id.llFooterBG)
@ -917,11 +930,11 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
val vFooterDivider2: View = viewRoot.findViewById(R.id.vFooterDivider2) val vFooterDivider2: View = viewRoot.findViewById(R.id.vFooterDivider2)
val vIndicator: View = viewRoot.findViewById(R.id.vIndicator) val vIndicator: View = viewRoot.findViewById(R.id.vIndicator)
val footerButtonBgColor = PrefI.ipFooterButtonBgColor(pref) val footerButtonBgColor = PrefI.ipFooterButtonBgColor.value
val footerButtonFgColor = PrefI.ipFooterButtonFgColor(pref) val footerButtonFgColor = PrefI.ipFooterButtonFgColor.value
val footerTabBgColor = PrefI.ipFooterTabBgColor(pref) val footerTabBgColor = PrefI.ipFooterTabBgColor.value
val footerTabDividerColor = PrefI.ipFooterTabDividerColor(pref) val footerTabDividerColor = PrefI.ipFooterTabDividerColor.value
val footerTabIndicatorColor = PrefI.ipFooterTabIndicatorColor(pref) val footerTabIndicatorColor = PrefI.ipFooterTabIndicatorColor.value
val colorColumnStripBackground = footerTabBgColor.notZero() val colorColumnStripBackground = footerTabBgColor.notZero()
?: activity.attrColor(R.attr.colorColumnStripBackground) ?: activity.attrColor(R.attr.colorColumnStripBackground)
@ -1023,6 +1036,21 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
} }
} }
} }
action(R.string.test_progress_dialog){
action={
launchAndShowError {
runInProgress(cancellable=true) {
it.setMessage("message")
it.setTitle("title")
delay(2000L)
it.setMessage("message ".repeat(30))
it.setTitle("title ".repeat(30))
delay(2000L)
}
}
}
}
} }
action(R.string.app_data_export) { action(R.string.app_data_export) {

View File

@ -0,0 +1,118 @@
package jp.juggler.subwaytooter.auth
import android.content.Context
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.auth.Auth2Result
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.notification.checkNotificationImmediate
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
import jp.juggler.subwaytooter.pref.PrefL
import jp.juggler.subwaytooter.pref.lazyContext
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.appDatabase
import jp.juggler.util.log.LogCategory
val Context.authRepo
get() = AuthRepo(
context = this,
daoAcctColor = AcctColor.Access(appDatabase),
daoSavedAccount = SavedAccount.Access(appDatabase, lazyContext),
)
class AuthRepo(
private val context: Context = lazyContext,
private val daoAcctColor: AcctColor.Access =
AcctColor.Access(appDatabase),
private val daoSavedAccount: SavedAccount.Access =
SavedAccount.Access(appDatabase, lazyContext),
) {
companion object {
private val log = LogCategory("AuthRepo")
}
/**
* ユーザ登録の確認手順が完了しているかどうか
*
* - マストドン以外だと何もしないはず
*/
suspend fun checkConfirmed(item: SavedAccount, client: TootApiClient) {
// 承認待ち状態ではないならチェックしない
if (item.loginAccount?.id != EntityId.CONFIRMING) return
// DBに保存されていないならチェックしない
if (item.db_id == SavedAccount.INVALID_DB_ID) return
// アクセストークンがないならチェックしない
val accessToken = item.bearerAccessToken ?: return
// ユーザ情報を取得してみる。承認済みなら読めるはず
// 読めなければ例外が出る
val userJson = client.verifyAccount(
accessToken = accessToken,
outTokenInfo = null,
misskeyVersion = 0, // Mastodon only
)
// 読めたらアプリ内の記録を更新する
TootParser(context, item).account(userJson)?.let { ta ->
item.loginAccount = ta
daoSavedAccount.saveSetting(item)
checkNotificationImmediateAll(context, onlySubscription = true)
checkNotificationImmediate(context, item.db_id)
}
}
fun accountRemove(account: SavedAccount) {
// if account is default account of tablet mode,
// reset default.
if (account.db_id == PrefL.lpTabletTootDefaultAccount.value) {
PrefL.lpTabletTootDefaultAccount.value = -1L
}
daoSavedAccount.delete(account.db_id)
// appServerUnregister(context.applicationContextSafe, account)
}
fun updateTokenInfo(item: SavedAccount, auth2Result: Auth2Result) {
item.token_info = auth2Result.tokenJson
item.loginAccount = auth2Result.tootAccount
item.misskeyVersion = auth2Result.tootInstance.misskeyVersionMajor
daoSavedAccount.saveSetting(item)
}
// notification_tagがもう使われてない
// private fun appServerUnregister(context: Context, account: SavedAccount) {
// launchIO {
// try {
// val installId = PrefDevice.from(context).getString(PrefDevice.KEY_INSTALL_ID, null)
// if (installId?.isEmpty() != false) {
// error("missing install_id")
// }
//
// val tag = "" // notification_tagはもう使われてない
// if (tag.isNullO) {
// error("missing notification_tag")
// }
//
// val call = App1.ok_http_client.newCall(
// "instance_url=${
// "https://${account.apiHost.ascii}".encodePercent()
// }&app_id=${
// context.packageName.encodePercent()
// }&tag=$tag"
// .toFormRequestBody()
// .toPost()
// .url("$APP_SERVER/unregister")
// .build()
// )
//
// val response = call.await()
// if (!response.isSuccessful) {
// log.e("appServerUnregister: $response")
// }
// } catch (ex: Throwable) {
// log.e(ex, "appServerUnregister failed.")
// }
// }
// }
}

View File

@ -1,7 +1,6 @@
package jp.juggler.subwaytooter.column package jp.juggler.subwaytooter.column
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.util.SparseArray import android.util.SparseArray
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.AppState import jp.juggler.subwaytooter.AppState
@ -13,6 +12,7 @@ import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.streaming.StreamCallback import jp.juggler.subwaytooter.streaming.StreamCallback
import jp.juggler.subwaytooter.streaming.StreamStatus import jp.juggler.subwaytooter.streaming.StreamStatus
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.BucketList import jp.juggler.subwaytooter.util.BucketList
import jp.juggler.subwaytooter.util.ScrollPosition import jp.juggler.subwaytooter.util.ScrollPosition
import jp.juggler.util.data.* import jp.juggler.util.data.*
@ -64,10 +64,10 @@ class Column(
internal const val QUICK_FILTER_VOTE = 6 internal const val QUICK_FILTER_VOTE = 6
internal const val QUICK_FILTER_POST = 7 internal const val QUICK_FILTER_POST = 7
fun loadAccount(context: Context, src: JsonObject): SavedAccount { fun loadAccount(src: JsonObject): SavedAccount {
val account_db_id = src.long(ColumnEncoder.KEY_ACCOUNT_ROW_ID) ?: -1L val account_db_id = src.long(ColumnEncoder.KEY_ACCOUNT_ROW_ID) ?: -1L
return if (account_db_id >= 0) { return if (account_db_id > 0) {
SavedAccount.loadAccount(context, account_db_id) daoSavedAccount.loadAccount(account_db_id)
?: error("missing account") ?: error("missing account")
} else { } else {
SavedAccount.na SavedAccount.na
@ -91,24 +91,24 @@ class Column(
var defaultColorContentAcct = 0 var defaultColorContentAcct = 0
var defaultColorContentText = 0 var defaultColorContentText = 0
fun reloadDefaultColor(activity: AppCompatActivity, pref: SharedPreferences) { fun reloadDefaultColor(activity: AppCompatActivity) {
defaultColorHeaderBg = PrefI.ipCcdHeaderBg(pref).notZero() defaultColorHeaderBg = PrefI.ipCcdHeaderBg.value.notZero()
?: activity.attrColor(R.attr.color_column_header) ?: activity.attrColor(R.attr.color_column_header)
defaultColorHeaderName = PrefI.ipCcdHeaderFg(pref).notZero() defaultColorHeaderName = PrefI.ipCcdHeaderFg.value.notZero()
?: activity.attrColor(R.attr.colorColumnHeaderName) ?: activity.attrColor(R.attr.colorColumnHeaderName)
defaultColorHeaderPageNumber = PrefI.ipCcdHeaderFg(pref).notZero() defaultColorHeaderPageNumber = PrefI.ipCcdHeaderFg.value.notZero()
?: activity.attrColor(R.attr.colorColumnHeaderPageNumber) ?: activity.attrColor(R.attr.colorColumnHeaderPageNumber)
defaultColorContentBg = PrefI.ipCcdContentBg(pref) defaultColorContentBg = PrefI.ipCcdContentBg.value
// may zero // may zero
defaultColorContentAcct = PrefI.ipCcdContentAcct(pref).notZero() defaultColorContentAcct = PrefI.ipCcdContentAcct.value.notZero()
?: activity.attrColor(R.attr.colorTimeSmall) ?: activity.attrColor(R.attr.colorTimeSmall)
defaultColorContentText = PrefI.ipCcdContentText(pref).notZero() defaultColorContentText = PrefI.ipCcdContentText.value.notZero()
?: activity.attrColor(R.attr.colorTextContent) ?: activity.attrColor(R.attr.colorTextContent)
} }
@ -257,7 +257,7 @@ class Column(
var keywordFilterTrees: FilterTrees? = null var keywordFilterTrees: FilterTrees? = null
@Volatile @Volatile
var favMuteSet: HashSet<Acct>? = null var favMuteSet: Set<Acct>? = null
@Volatile @Volatile
var highlightTrie: WordTrieTree? = null var highlightTrie: WordTrieTree? = null
@ -341,7 +341,7 @@ class Column(
internal constructor(appState: AppState, src: JsonObject) : this( internal constructor(appState: AppState, src: JsonObject) : this(
appState, appState,
appState.context, appState.context,
loadAccount(appState.context, src), loadAccount(src),
src.optInt(ColumnEncoder.KEY_TYPE), src.optInt(ColumnEncoder.KEY_TYPE),
ColumnEncoder.decodeColumnId(src) ColumnEncoder.decodeColumnId(src)
) { ) {

View File

@ -192,7 +192,7 @@ fun Column.removeNotifications() {
listData.clear() listData.clear()
duplicateMap.clear() duplicateMap.clear()
fireShowContent(reason = "removeNotifications", reset = true) fireShowContent(reason = "removeNotifications", reset = true)
onNotificationCleared(context, accessInfo.db_id) onNotificationCleared(accessInfo.db_id)
} }
// 通知を削除した後に呼ばれる // 通知を削除した後に呼ばれる

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter.column package jp.juggler.subwaytooter.column
import jp.juggler.subwaytooter.api.entity.EntityId import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.util.data.JsonException import jp.juggler.util.data.JsonException
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.encodeBase64Url import jp.juggler.util.data.encodeBase64Url
@ -248,11 +248,11 @@ object ColumnEncoder {
} }
// 以下は保存には必要ないが、カラムリスト画面で使う // 以下は保存には必要ないが、カラムリスト画面で使う
val ac = AcctColor.load(accessInfo) val ac = daoAcctColor.load(accessInfo)
dst[KEY_COLUMN_ACCESS_ACCT] = accessInfo.acct.ascii dst[KEY_COLUMN_ACCESS_ACCT] = accessInfo.acct.ascii
dst[KEY_COLUMN_ACCESS_STR] = ac.nickname dst[KEY_COLUMN_ACCESS_STR] = ac.nickname
dst[KEY_COLUMN_ACCESS_COLOR] = ac.color_fg dst[KEY_COLUMN_ACCESS_COLOR] = ac.colorFg
dst[KEY_COLUMN_ACCESS_COLOR_BG] = ac.color_bg dst[KEY_COLUMN_ACCESS_COLOR_BG] = ac.colorBg
dst[KEY_COLUMN_NAME] = getColumnName(true) dst[KEY_COLUMN_NAME] = getColumnName(true)
dst[KEY_OLD_INDEX] = oldIndex dst[KEY_OLD_INDEX] = oldIndex
} }

View File

@ -197,7 +197,7 @@ fun Column.onActivityStart() {
if (!bRefreshLoading && if (!bRefreshLoading &&
canAutoRefresh() && canAutoRefresh() &&
!PrefB.bpDontRefreshOnResume(appState.pref) && !PrefB.bpDontRefreshOnResume.value &&
!dontAutoRefresh !dontAutoRefresh
) { ) {
// リフレッシュしてからストリーミング開始 // リフレッシュしてからストリーミング開始
@ -237,7 +237,7 @@ fun Column.startLoading() {
initFilter() initFilter()
Column.showOpenSticker = PrefB.bpOpenSticker(appState.pref) Column.showOpenSticker = PrefB.bpOpenSticker.value
mRefreshLoadingErrorPopupState = 0 mRefreshLoadingErrorPopupState = 0
mRefreshLoadingError = "" mRefreshLoadingError = ""

View File

@ -7,10 +7,7 @@ import jp.juggler.subwaytooter.api.ApiTask
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.table.FavMute import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.WordTrieTree import jp.juggler.util.data.WordTrieTree
@ -171,8 +168,8 @@ fun Column.initFilter() {
} }
} }
favMuteSet = FavMute.acctSet favMuteSet = daoFavMute.acctSet()
highlightTrie = HighlightWord.nameSet highlightTrie = daoHighlightWord.nameSet()
} }
private fun Column.isFilteredByAttachment(status: TootStatus): Boolean { private fun Column.isFilteredByAttachment(status: TootStatus): Boolean {
@ -252,10 +249,10 @@ fun Column.isFiltered(status: TootStatus): Boolean {
if (checkLanguageFilter(status)) return true if (checkLanguageFilter(status)) return true
if (accessInfo.isPseudo) { if (accessInfo.isPseudo) {
var r = UserRelation.loadPseudo(accessInfo.getFullAcct(status.account)) var r = daoUserRelation.loadPseudo(accessInfo.getFullAcct(status.account))
if (r.muting || r.blocking) return true if (r.muting || r.blocking) return true
if (reblog != null) { if (reblog != null) {
r = UserRelation.loadPseudo(accessInfo.getFullAcct(reblog.account)) r = daoUserRelation.loadPseudo(accessInfo.getFullAcct(reblog.account))
if (r.muting || r.blocking) return true if (r.muting || r.blocking) return true
} }
} }

View File

@ -167,8 +167,8 @@ fun Column.mergeStreamingMessage() {
App1.sound(it) App1.sound(it)
} }
} }
o.highlightSpeech?.let { o.highlightSpeech?.name?.notEmpty()?.let {
appState.addSpeech(it.name, dedupMode = DedupMode.RecentExpire) appState.addSpeech(it, dedupMode = DedupMode.RecentExpire)
} }
} }
} }

View File

@ -1,7 +1,6 @@
package jp.juggler.subwaytooter.column package jp.juggler.subwaytooter.column
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.os.SystemClock import android.os.SystemClock
import jp.juggler.subwaytooter.api.ApiPath import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiClient
@ -72,9 +71,6 @@ abstract class ColumnTask(
val misskeyVersion: Int val misskeyVersion: Int
get() = accessInfo.misskeyVersion get() = accessInfo.misskeyVersion
val pref: SharedPreferences
get() = column.appState.pref
internal fun JsonObject.addMisskeyNotificationFilter() = addMisskeyNotificationFilter(column) internal fun JsonObject.addMisskeyNotificationFilter() = addMisskeyNotificationFilter(column)
internal fun JsonObject.addRangeMisskey(bBottom: Boolean) = addRangeMisskey(column, bBottom) internal fun JsonObject.addRangeMisskey(bBottom: Boolean) = addRangeMisskey(column, bBottom)

View File

@ -128,7 +128,7 @@ class ColumnTask_Gap(
val iv = when { val iv = when {
isHead -> PrefI.ipGapHeadScrollPosition isHead -> PrefI.ipGapHeadScrollPosition
else -> PrefI.ipGapTailScrollPosition else -> PrefI.ipGapTailScrollPosition
}.invoke(pref) }.value
val scrollHead = iv == PrefI.GSP_HEAD val scrollHead = iv == PrefI.GSP_HEAD
if (scrollHead) { if (scrollHead) {

View File

@ -5,6 +5,7 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.* import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.auth.authRepo
import jp.juggler.subwaytooter.columnviewholder.scrollToTop import jp.juggler.subwaytooter.columnviewholder.scrollToTop
import jp.juggler.subwaytooter.notification.injectData import jp.juggler.subwaytooter.notification.injectData
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
@ -33,7 +34,7 @@ class ColumnTask_Loading(
override suspend fun background(): TootApiResult? { override suspend fun background(): TootApiResult? {
ctStarted.set(true) ctStarted.set(true)
if (PrefB.bpOpenSticker(pref)) { if (PrefB.bpOpenSticker.value) {
OpenSticker.loadAndWait() OpenSticker.loadAndWait()
} }
@ -52,7 +53,7 @@ class ColumnTask_Loading(
client.account = accessInfo client.account = accessInfo
try { try {
accessInfo.checkConfirmed(context, client) context.authRepo.checkConfirmed(accessInfo, client)
column.keywordFilterTrees = column.encodeFilterTree(column.loadFilter2(client)) column.keywordFilterTrees = column.encodeFilterTree(column.loadFilter2(client))

View File

@ -16,6 +16,7 @@ import jp.juggler.util.coroutine.runOnMainLooper
import jp.juggler.util.coroutine.runOnMainLooperDelayed import jp.juggler.util.coroutine.runOnMainLooperDelayed
import jp.juggler.util.data.JsonArray import jp.juggler.util.data.JsonArray
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.withCaption import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
@ -157,8 +158,8 @@ class ColumnTask_Refresh(
App1.sound(it) App1.sound(it)
} }
} }
o.highlightSpeech?.let { o.highlightSpeech?.name.notEmpty()?.let {
column.appState.addSpeech(it.name, dedupMode = DedupMode.RecentExpire) column.appState.addSpeech(it, dedupMode = DedupMode.RecentExpire)
} }
} }
} }
@ -306,7 +307,7 @@ class ColumnTask_Refresh(
isCancelled -> false isCancelled -> false
listTmp?.isNotEmpty() != true -> false listTmp?.isNotEmpty() != true -> false
willAddGap -> true willAddGap -> true
else -> PrefB.bpForceGap() else -> PrefB.bpForceGap.value
} }
if (doesAddGap()) { if (doesAddGap()) {
@ -495,7 +496,7 @@ class ColumnTask_Refresh(
if (!isCancelled && if (!isCancelled &&
listTmp?.isNotEmpty() == true && listTmp?.isNotEmpty() == true &&
(willAddGap || PrefB.bpForceGap(context)) (willAddGap || PrefB.bpForceGap.value)
) { ) {
addOne(listTmp, TootGap.mayNull(maxId, lastSinceId), head = addToHead) addOne(listTmp, TootGap.mayNull(maxId, lastSinceId), head = addToHead)
} }
@ -582,7 +583,7 @@ class ColumnTask_Refresh(
if (!isCancelled && if (!isCancelled &&
listTmp?.isNotEmpty() == true && listTmp?.isNotEmpty() == true &&
(willAddGap || PrefB.bpForceGap(context)) (willAddGap || PrefB.bpForceGap.value)
) { ) {
addOne(listTmp, TootGap.mayNull(maxId, lastSinceId), head = addToHead) addOne(listTmp, TootGap.mayNull(maxId, lastSinceId), head = addToHead)
} }
@ -690,16 +691,14 @@ class ColumnTask_Refresh(
params.apply { params.apply {
if (!bBottom) { if (!bBottom) {
if (first) { if (first) {
addRangeMisskey(bBottom = false)
addRangeMisskey(bBottom)
} else { } else {
putMisskeySince(column.idRecent) putMisskeySince(column.idRecent)
} }
} else { } else {
if (first) { if (first) {
when (column.pagingType) { when (column.pagingType) {
ColumnPagingType.Default -> addRangeMisskey(bBottom) ColumnPagingType.Default -> addRangeMisskey(bBottom = true)
ColumnPagingType.Offset -> put("offset", column.offsetNext) ColumnPagingType.Offset -> put("offset", column.offsetNext)
ColumnPagingType.Cursor -> put("cursor", column.idOld) ColumnPagingType.Cursor -> put("cursor", column.idOld)

View File

@ -15,7 +15,7 @@ import jp.juggler.subwaytooter.search.NotestockHelper.refreshNotestock
import jp.juggler.subwaytooter.search.TootsearchHelper.loadingTootsearch import jp.juggler.subwaytooter.search.TootsearchHelper.loadingTootsearch
import jp.juggler.subwaytooter.search.TootsearchHelper.refreshTootsearch import jp.juggler.subwaytooter.search.TootsearchHelper.refreshTootsearch
import jp.juggler.subwaytooter.streaming.StreamSpec import jp.juggler.subwaytooter.streaming.StreamSpec
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -685,7 +685,7 @@ enum class ColumnType(
R.string.profile_of, R.string.profile_of,
when (who) { when (who) {
null -> profileId.toString() null -> profileId.toString()
else -> AcctColor.getNickname(accessInfo, who) else -> daoAcctColor.getNickname(accessInfo, who)
} }
) )
}, },

View File

@ -3,12 +3,13 @@ package jp.juggler.subwaytooter.column
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.AcctSet import jp.juggler.subwaytooter.table.daoAcctSet
import jp.juggler.subwaytooter.table.TagSet import jp.juggler.subwaytooter.table.daoTagHistory
import jp.juggler.subwaytooter.table.UserRelation import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.util.data.toJsonArray import jp.juggler.util.data.toJsonArray
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import kotlin.math.min
class UserRelationLoader(val column: Column) { class UserRelationLoader(val column: Column) {
companion object { companion object {
@ -63,7 +64,13 @@ class UserRelationLoader(val column: Column) {
while (start < end) { while (start < end) {
var step = end - start var step = end - start
if (step > Column.RELATIONSHIP_LOAD_STEP) step = Column.RELATIONSHIP_LOAD_STEP if (step > Column.RELATIONSHIP_LOAD_STEP) step = Column.RELATIONSHIP_LOAD_STEP
UserRelation.saveListMisskey(now, column.accessInfo.db_id, whoList, start, step) daoUserRelation.saveListMisskey(
now,
column.accessInfo.db_id,
whoList,
start,
step
)
start += step start += step
} }
log.d("updateRelation: update $end relations.") log.d("updateRelation: update $end relations.")
@ -108,7 +115,11 @@ class UserRelationLoader(val column: Column) {
for (i in 0 until list.size) { for (i in 0 until list.size) {
list[i].id = userIdList[i] list[i].id = userIdList[i]
} }
UserRelation.saveListMisskeyRelationApi(now, column.accessInfo.db_id, list) daoUserRelation.saveListMisskeyRelationApi(
now,
column.accessInfo.db_id,
list
)
} }
} }
log.d("updateRelation: update $n relations.") log.d("updateRelation: update $n relations.")
@ -134,7 +145,7 @@ class UserRelationLoader(val column: Column) {
} }
val result = client.request(sb.toString()) ?: break // cancelled. val result = client.request(sb.toString()) ?: break // cancelled.
val list = parseList(::TootRelationShip, parser, result.jsonArray) val list = parseList(::TootRelationShip, parser, result.jsonArray)
if (list.size > 0) UserRelation.saveListMastodon( if (list.size > 0) daoUserRelation.saveListMastodon(
now, now,
column.accessInfo.db_id, column.accessInfo.db_id,
list list
@ -156,7 +167,7 @@ class UserRelationLoader(val column: Column) {
while (n < acctList.size) { while (n < acctList.size) {
var length = size - n var length = size - n
if (length > Column.ACCT_DB_STEP) length = Column.ACCT_DB_STEP if (length > Column.ACCT_DB_STEP) length = Column.ACCT_DB_STEP
AcctSet.saveList(now, acctList, n, length) daoAcctSet.saveList(now, acctList, n, length)
n += length n += length
} }
log.d("updateRelation: update $n acct.") log.d("updateRelation: update $n acct.")
@ -171,11 +182,10 @@ class UserRelationLoader(val column: Column) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
n = 0 n = 0
while (n < tagList.size) { while (n < size) {
var length = size - n val step = min(Column.ACCT_DB_STEP, size - n)
if (length > Column.ACCT_DB_STEP) length = Column.ACCT_DB_STEP daoTagHistory.saveList(now, tagList, n, step)
TagSet.saveList(now, tagList, n, length) n += step
n += length
} }
log.d("updateRelation: update $n tag.") log.d("updateRelation: update $n tag.")
} }

View File

@ -24,7 +24,7 @@ import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.column.* import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.streaming.* import jp.juggler.subwaytooter.streaming.*
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.MyLinkMovementMethod import jp.juggler.subwaytooter.view.MyLinkMovementMethod
import jp.juggler.subwaytooter.view.MyTextView import jp.juggler.subwaytooter.view.MyTextView
@ -219,15 +219,15 @@ class ColumnViewHolder(
val column = this.column val column = this.column
if (column == null || column.isDispose.get()) return@Runnable if (column == null || column.isDispose.get()) return@Runnable
val ac = AcctColor.load(column.accessInfo) val ac = daoAcctColor.load(column.accessInfo)
tvColumnContext.text = ac.nickname tvColumnContext.text = ac.nickname
tvColumnContext.setTextColor( tvColumnContext.setTextColor(
ac.color_fg.notZero() ac.colorFg.notZero()
?: activity.attrColor(R.attr.colorTimeSmall) ?: activity.attrColor(R.attr.colorTimeSmall)
) )
tvColumnContext.setBackgroundColor(ac.color_bg) tvColumnContext.setBackgroundColor(ac.colorBg)
tvColumnContext.setPaddingRelative(activity.acctPadLr, 0, activity.acctPadLr, 0) tvColumnContext.setPaddingRelative(activity.acctPadLr, 0, activity.acctPadLr, 0)
tvColumnName.text = column.getColumnName(false) tvColumnName.text = column.getColumnName(false)
@ -358,7 +358,7 @@ class ColumnViewHolder(
} }
} }
if (PrefB.bpShareViewPool(activity.pref)) { if (PrefB.bpShareViewPool.value) {
listView.setRecycledViewPool(activity.viewPool) listView.setRecycledViewPool(activity.viewPool)
} }
listView.itemAnimator = null listView.itemAnimator = null
@ -432,7 +432,7 @@ class ColumnViewHolder(
cbWithHighlight, cbWithHighlight,
).forEach { it.setOnCheckedChangeListener(this) } ).forEach { it.setOnCheckedChangeListener(this) }
if (PrefB.bpMoveNotificationsQuickFilter(activity.pref)) { if (PrefB.bpMoveNotificationsQuickFilter.value) {
(svQuickFilter.parent as? ViewGroup)?.removeView(svQuickFilter) (svQuickFilter.parent as? ViewGroup)?.removeView(svQuickFilter)
llColumnSettingInside.addView(svQuickFilter, 0) llColumnSettingInside.addView(svQuickFilter, 0)

View File

@ -337,7 +337,7 @@ private fun ColumnViewHolder.showReactions(
) )
val actMain = activity val actMain = activity
val disableEmojiAnimation = PrefB.bpDisableEmojiAnimation(actMain.pref) val disableEmojiAnimation = PrefB.bpDisableEmojiAnimation.value
for (reaction in reactions) { for (reaction in reactions) {

View File

@ -48,7 +48,7 @@ fun ColumnViewHolder.closeBitmaps() {
fun ColumnViewHolder.loadBackgroundImage(iv: ImageView, url: String?) { fun ColumnViewHolder.loadBackgroundImage(iv: ImageView, url: String?) {
try { try {
if (url == null || url.isEmpty() || PrefB.bpDontShowColumnBackgroundImage(activity.pref)) { if (url == null || url.isEmpty() || PrefB.bpDontShowColumnBackgroundImage.value) {
// 指定がないなら閉じる // 指定がないなら閉じる
closeBitmaps() closeBitmaps()
return return
@ -128,7 +128,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, pageIdx: Int, pageCount: Int)
ColumnViewHolder.log.d("onPageCreate [$pageIdx] ${column.getColumnName(true)}") ColumnViewHolder.log.d("onPageCreate [$pageIdx] ${column.getColumnName(true)}")
val bSimpleList = !column.isConversation && PrefB.bpSimpleList(activity.pref) val bSimpleList = !column.isConversation && PrefB.bpSimpleList.value
tvColumnIndex.text = activity.getString(R.string.column_index, pageIdx + 1, pageCount) tvColumnIndex.text = activity.getString(R.string.column_index, pageIdx + 1, pageCount)
tvColumnStatus.text = "?" tvColumnStatus.text = "?"
@ -221,7 +221,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, pageIdx: Int, pageCount: Int)
btnEmojiAdd.vg(false) btnEmojiAdd.vg(false)
etSearch.vg(true) etSearch.vg(true)
btnSearchClear.vg(PrefB.bpShowSearchClear(activity.pref)) btnSearchClear.vg(PrefB.bpShowSearchClear.value)
cbResolve.vg(column.type == ColumnType.SEARCH) cbResolve.vg(column.type == ColumnType.SEARCH)
} }
@ -281,7 +281,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, pageIdx: Int, pageCount: Int)
fun dip(dp: Int): Int = (activity.density * dp + 0.5f).toInt() fun dip(dp: Int): Int = (activity.density * dp + 0.5f).toInt()
val context = activity val context = activity
val announcementsBgColor = PrefI.ipAnnouncementsBgColor().notZero() val announcementsBgColor = PrefI.ipAnnouncementsBgColor.value.notZero()
?: context.attrColor(R.attr.colorSearchFormBackground) ?: context.attrColor(R.attr.colorSearchFormBackground)
btnAnnouncementsCutout.apply { btnAnnouncementsCutout.apply {
@ -294,7 +294,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, pageIdx: Int, pageCount: Int)
setPadding(0, padV, 0, padV) setPadding(0, padV, 0, padV)
} }
val searchBgColor = PrefI.ipSearchBgColor().notZero() val searchBgColor = PrefI.ipSearchBgColor.value.notZero()
?: context.attrColor(R.attr.colorSearchFormBackground) ?: context.attrColor(R.attr.colorSearchFormBackground)
llSearch.apply { llSearch.apply {

View File

@ -30,7 +30,7 @@ fun ColumnViewHolder.showQuickFilter() {
btnQuickFilterReaction.vg(column.isMisskey) btnQuickFilterReaction.vg(column.isMisskey)
btnQuickFilterFavourite.vg(!column.isMisskey) btnQuickFilterFavourite.vg(!column.isMisskey)
val insideColumnSetting = PrefB.bpMoveNotificationsQuickFilter(activity.pref) val insideColumnSetting = PrefB.bpMoveNotificationsQuickFilter.value
val showQuickFilterButton: (btn: View, iconId: Int, selected: Boolean) -> Unit val showQuickFilterButton: (btn: View, iconId: Int, selected: Boolean) -> Unit

View File

@ -28,9 +28,7 @@ import jp.juggler.subwaytooter.span.EmojiImageSpan
import jp.juggler.subwaytooter.span.LinkInfo import jp.juggler.subwaytooter.span.LinkInfo
import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.span.createSpan import jp.juggler.subwaytooter.span.createSpan
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.subwaytooter.util.openCustomTab import jp.juggler.subwaytooter.util.openCustomTab
@ -356,7 +354,7 @@ internal class ViewHolderHeaderProfile(
whoDetail?.statuses_count ?: who.statuses_count whoDetail?.statuses_count ?: who.statuses_count
}" }"
val hideFollowCount = PrefB.bpHideFollowCount(activity.pref) val hideFollowCount = PrefB.bpHideFollowCount.value
var caption = activity.getString(R.string.following) var caption = activity.getString(R.string.following)
btnFollowing.text = when { btnFollowing.text = when {
@ -370,7 +368,7 @@ internal class ViewHolderHeaderProfile(
else -> "${caption}\n${whoDetail?.followers_count ?: who.followers_count}" else -> "${caption}\n${whoDetail?.followers_count ?: who.followers_count}"
} }
val relation = UserRelation.load(accessInfo.db_id, who.id) val relation = daoUserRelation.load(accessInfo.db_id, who.id)
this.relation = relation this.relation = relation
setFollowIcon( setFollowIcon(
activity, activity,
@ -414,7 +412,7 @@ internal class ViewHolderHeaderProfile(
setAcct(tvMovedAcct, accessInfo, moved) setAcct(tvMovedAcct, accessInfo, moved)
val relation = UserRelation.load(accessInfo.db_id, moved.id) val relation = daoUserRelation.load(accessInfo.db_id, moved.id)
setFollowIcon( setFollowIcon(
activity, activity,
btnMoved, btnMoved,
@ -482,7 +480,11 @@ internal class ViewHolderHeaderProfile(
val lastColumn = column val lastColumn = column
DlgTextInput.show( DlgTextInput.show(
activity, activity,
AcctColor.getStringWithNickname(activity, R.string.personal_notes_of, who.acct), daoAcctColor.getStringWithNickname(
activity,
R.string.personal_notes_of,
who.acct
),
relation?.note ?: "", relation?.note ?: "",
allowEmpty = true, allowEmpty = true,
callback = object : DlgTextInput.Callback { callback = object : DlgTextInput.Callback {
@ -551,16 +553,16 @@ internal class ViewHolderHeaderProfile(
} }
private fun setAcct(tv: TextView, accessInfo: SavedAccount, who: TootAccount) { private fun setAcct(tv: TextView, accessInfo: SavedAccount, who: TootAccount) {
val ac = AcctColor.load(accessInfo, who) val ac = daoAcctColor.load(accessInfo, who)
tv.text = when { tv.text = when {
AcctColor.hasNickname(ac) -> ac.nickname daoAcctColor.hasNickname(ac) -> ac.nickname
PrefB.bpShortAcctLocalUser() -> "@${who.acct.pretty}" PrefB.bpShortAcctLocalUser.value -> "@${who.acct.pretty}"
else -> "@${ac.nickname}" else -> "@${ac.nickname}"
} }
tv.textColor = ac.color_fg.notZero() ?: column.getAcctColor() tv.textColor = ac.colorFg.notZero() ?: column.getAcctColor()
tv.setBackgroundColor(ac.color_bg) // may 0 tv.setBackgroundColor(ac.colorBg) // may 0
tv.setPaddingRelative(activity.acctPadLr, 0, activity.acctPadLr, 0) tv.setPaddingRelative(activity.acctPadLr, 0, activity.acctPadLr, 0)
} }
@ -594,7 +596,7 @@ internal class ViewHolderHeaderProfile(
when { when {
emoji == null -> emoji == null ->
append("locked") append("locked")
PrefB.bpUseTwemoji() -> PrefB.bpUseTwemoji.value ->
appendSpan("locked", emoji.createSpan(activity)) appendSpan("locked", emoji.createSpan(activity))
else -> else ->
append(emoji.unifiedCode) append(emoji.unifiedCode)
@ -607,7 +609,7 @@ internal class ViewHolderHeaderProfile(
when { when {
emoji == null -> emoji == null ->
append("bot") append("bot")
PrefB.bpUseTwemoji() -> PrefB.bpUseTwemoji.value ->
appendSpan("bot", emoji.createSpan(activity)) appendSpan("bot", emoji.createSpan(activity))
else -> else ->
append(emoji.unifiedCode) append(emoji.unifiedCode)
@ -620,7 +622,7 @@ internal class ViewHolderHeaderProfile(
when { when {
emoji == null -> emoji == null ->
append("suspended") append("suspended")
PrefB.bpUseTwemoji() -> PrefB.bpUseTwemoji.value ->
appendSpan("suspended", emoji.createSpan(activity)) appendSpan("suspended", emoji.createSpan(activity))
else -> else ->
append(emoji.unifiedCode) append(emoji.unifiedCode)
@ -715,7 +717,7 @@ internal class ViewHolderHeaderProfile(
valueText.append(TootStatus.formatTime(activity, item.verified_at, false)) valueText.append(TootStatus.formatTime(activity, item.verified_at, false))
val end = valueText.length val end = valueText.length
val linkFgColor = PrefI.ipVerifiedLinkFgColor(activity.pref).notZero() val linkFgColor = PrefI.ipVerifiedLinkFgColor.value.notZero()
?: (Color.BLACK or 0x7fbc99) ?: (Color.BLACK or 0x7fbc99)
valueText.setSpan( valueText.setSpan(
@ -737,7 +739,7 @@ internal class ViewHolderHeaderProfile(
valueView.movementMethod = MyLinkMovementMethod valueView.movementMethod = MyLinkMovementMethod
if (item.verified_at > 0L) { if (item.verified_at > 0L) {
val linkBgColor = PrefI.ipVerifiedLinkBgColor(activity.pref).notZero() val linkBgColor = PrefI.ipVerifiedLinkBgColor.value.notZero()
?: (0x337fbc99) ?: (0x337fbc99)
valueView.setBackgroundColor(linkBgColor) valueView.setBackgroundColor(linkBgColor)

View File

@ -3,9 +3,7 @@ package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.RelativeSizeSpan
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
@ -13,8 +11,7 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatButton
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.ui.dismissSafe import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.getAdaptiveRippleDrawableRound import jp.juggler.util.ui.getAdaptiveRippleDrawableRound
@ -30,7 +27,7 @@ suspend fun AppCompatActivity.pickAccount(
bAllowMastodon: Boolean = true, bAllowMastodon: Boolean = true,
bAuto: Boolean = false, bAuto: Boolean = false,
message: String? = null, message: String? = null,
accountListArg: MutableList<SavedAccount>? = null, accountListArg: List<SavedAccount>? = null,
dismissCallback: (dialog: DialogInterface) -> Unit = {}, dismissCallback: (dialog: DialogInterface) -> Unit = {},
extraCallback: (LinearLayout, Int, Int) -> Unit = { _, _, _ -> }, extraCallback: (LinearLayout, Int, Int) -> Unit = { _, _, _ -> },
): SavedAccount? { ): SavedAccount? {
@ -54,11 +51,10 @@ suspend fun AppCompatActivity.pickAccount(
else -> 0 else -> 0
} }
val accountList: MutableList<SavedAccount> = accountListArg val accountList = accountListArg
?: SavedAccount.loadAccountList(activity) ?: daoSavedAccount.loadAccountList()
.filter { 0 == it.checkMastodon() + it.checkMisskey() + it.checkPseudo() } .filter { 0 == it.checkMastodon() + it.checkMisskey() + it.checkPseudo() }
.toMutableList() .sortedByNickname()
.also { SavedAccount.sort(it) }
if (accountList.isEmpty()) { if (accountList.isEmpty()) {
@ -127,17 +123,21 @@ suspend fun AppCompatActivity.pickAccount(
LinearLayout.LayoutParams.WRAP_CONTENT LinearLayout.LayoutParams.WRAP_CONTENT
) )
val ac = AcctColor.load(a) val ac = daoAcctColor.load(a)
val b = AppCompatButton(activity) val b = AppCompatButton(activity)
if (AcctColor.hasColorBackground(ac)) { if (daoAcctColor.hasColorBackground(ac)) {
b.background = getAdaptiveRippleDrawableRound(activity, ac.color_bg, ac.color_fg) b.background = getAdaptiveRippleDrawableRound(
activity,
ac.colorBg,
ac.colorFg
)
} else { } else {
b.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp) b.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
} }
if (AcctColor.hasColorForeground(ac)) { if (daoAcctColor.hasColorForeground(ac)) {
b.textColor = ac.color_fg b.textColor = ac.colorFg
} }
b.setPaddingRelative(padX, padY, padX, padY) b.setPaddingRelative(padX, padY, padX, padY)
@ -147,19 +147,20 @@ suspend fun AppCompatActivity.pickAccount(
b.minHeight = (0.5f + 32f * density).toInt() b.minHeight = (0.5f + 32f * density).toInt()
val sb = SpannableStringBuilder(ac.nickname) val sb = SpannableStringBuilder(ac.nickname)
if (a.lastNotificationError?.isNotEmpty() == true) { // TODO エラー状態を表示する
sb.append("\n") // if (a.lastNotificationError?.isNotEmpty() == true) {
val start = sb.length // sb.append("\n")
sb.append(a.lastNotificationError) // val start = sb.length
val end = sb.length // sb.append(a.lastNotificationError)
sb.setSpan(RelativeSizeSpan(0.7f), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) // val end = sb.length
} else if (a.last_subscription_error?.isNotEmpty() == true) { // sb.setSpan(RelativeSizeSpan(0.7f), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.append("\n") // } else if (a.last_subscription_error?.isNotEmpty() == true) {
val start = sb.length // sb.append("\n")
sb.append(a.last_subscription_error) // val start = sb.length
val end = sb.length // sb.append(a.last_subscription_error)
sb.setSpan(RelativeSizeSpan(0.7f), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) // val end = sb.length
} // sb.setSpan(RelativeSizeSpan(0.7f), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// }
b.text = sb b.text = sb
b.setOnClickListener { b.setOnClickListener {

View File

@ -1,35 +1,50 @@
package jp.juggler.subwaytooter.dialog package jp.juggler.subwaytooter.dialog
import android.content.Context import android.content.Context
import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.R
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
import jp.juggler.util.ui.dismissSafe
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.* import java.util.*
import kotlin.coroutines.resumeWithException
class ActionsDialog { class ActionsDialogInitializer(
val title: CharSequence? = null,
) {
class Action(val caption: CharSequence, val action: suspend () -> Unit)
private val actionList = ArrayList<Action>() val list = ArrayList<Action>()
private class Action(val caption: CharSequence, val action: () -> Unit) fun action(caption: CharSequence, action: suspend () -> Unit) {
list.add(Action(caption, action))
fun addAction(caption: CharSequence, action: () -> Unit): ActionsDialog {
actionList.add(Action(caption, action))
return this
} }
fun show(context: Context, title: CharSequence? = null): ActionsDialog { @OptIn(ExperimentalCoroutinesApi::class)
AlertDialog.Builder(context).apply { suspend fun showSuspend(context: Context): Action =
setNegativeButton(R.string.cancel, null) suspendCancellableCoroutine { cont ->
setItems(actionList.map { it.caption }.toTypedArray()) { _, which -> val dialog = android.app.AlertDialog.Builder(context).apply {
if (which >= 0 && which < actionList.size) { title?.notEmpty()?.let { setTitle(it) }
actionList[which].action() setNegativeButton(android.R.string.cancel, null)
setItems(list.map { it.caption }.toTypedArray()) { d, i ->
if (cont.isActive) cont.resume(list[i]) {}
d.dismissSafe()
} }
} setOnDismissListener {
title?.notEmpty()?.let { setTitle(it) } if (cont.isActive) cont.resumeWithException(CancellationException())
}.show() }
}.create()
return this cont.invokeOnCancellation { dialog.dismissSafe() }
} dialog.show()
}
}
suspend fun Context.actionsDialog(
title: String? = null,
init: suspend ActionsDialogInitializer.() -> Unit,
) {
ActionsDialogInitializer(title)
.apply { init() }
.showSuspend(this)
.action.invoke()
} }

View File

@ -64,13 +64,13 @@ object DlgConfirm {
// } // }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
suspend fun AppCompatActivity.confirm( suspend inline fun AppCompatActivity.confirm(
message: String, message: String,
getConfirmEnabled: Boolean, isConfirmEnabled: Boolean,
setConfirmEnabled: (newConfirmEnabled: Boolean) -> Unit, setConfirmEnabled: (newConfirmEnabled: Boolean) -> Unit,
) { ) {
if (!getConfirmEnabled) return if (!isConfirmEnabled) return
suspendCancellableCoroutine<Unit> { cont -> val skipNext = suspendCancellableCoroutine { cont ->
try { try {
val views = DlgConfirmBinding.inflate(layoutInflater) val views = DlgConfirmBinding.inflate(layoutInflater)
views.tvMessage.text = message views.tvMessage.text = message
@ -79,10 +79,7 @@ object DlgConfirm {
.setCancelable(true) .setCancelable(true)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
if (views.cbSkipNext.isChecked) { if (cont.isActive) cont.resume(views.cbSkipNext.isChecked)
setConfirmEnabled(false)
}
if (cont.isActive) cont.resume(Unit)
} }
dialog.setOnDismissListener { dialog.setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException("dialog cancelled.")) if (cont.isActive) cont.resumeWithException(CancellationException("dialog cancelled."))
@ -92,6 +89,7 @@ object DlgConfirm {
cont.resumeWithException(ex) cont.resumeWithException(ex)
} }
} }
if (skipNext) setConfirmEnabled(false)
} }
suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) = suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) =

Some files were not shown because too many files have changed in this diff Show More