とりあえずアプリサーバ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
get() = ctx.assets
inline val AnkoContext<*>.defaultSharedPreferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(ctx)
inline val Context.defaultSharedPreferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(this)
inline val Fragment.defaultSharedPreferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(requireContext())
//inline val AnkoContext<*>.defaultSharedPreferences: SharedPreferences
// get() = PreferenceManager.getDefaultSharedPreferences(ctx)
//
//inline val Context.defaultSharedPreferences: SharedPreferences
// get() = PreferenceManager.getDefaultSharedPreferences(this)
//
//inline val Fragment.defaultSharedPreferences: SharedPreferences
// get() = PreferenceManager.getDefaultSharedPreferences(requireContext())
inline val Fragment.act: Activity?
get() = activity

View File

@ -1,14 +1,17 @@
import io.gitlab.arturbosch.detekt.Detekt
import java.text.SimpleDateFormat
import java.util.regex.Matcher
import java.util.regex.Pattern
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
apply plugin: 'com.google.gms.google-services'
apply plugin: "io.gitlab.arturbosch.detekt"
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt")
id("org.jetbrains.kotlin.plugin.serialization")
id("com.google.devtools.ksp").version("1.8.0-1.0.9")
id("io.gitlab.arturbosch.detekt")
}
android {
compileSdkVersion stCompileSdkVersion
@ -58,25 +61,30 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
lintOptions {
disable 'MissingTranslation'
disable "MissingTranslation"
}
}
debug{
debug {
}
}
// Specifies comma-separated list of flavor dimensions.
flavorDimensions "rcOrDev"
flavorDimensions "fcmType"
productFlavors {
rc {
dimension "rcOrDev"
nofcm {
dimension "fcmType"
versionNameSuffix "-noFcm"
}
fcm {
dimension "fcmType"
versionNameSuffix "-play"
}
}
@ -97,18 +105,18 @@ android {
packagingOptions {
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
pickFirsts += ['META-INF/atomicfu.kotlin_module']
pickFirsts += ["META-INF/atomicfu.kotlin_module"]
}
}
useLibrary 'android.test.base'
useLibrary 'android.test.mock'
useLibrary "android.test.base"
useLibrary "android.test.mock"
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"
implementation(project(":base"))
implementation project(':colorpicker')
implementation project(':emoji')
implementation project(':apng_android')
implementation project(':anko')
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs')
implementation project(":colorpicker")
implementation project(":emoji")
implementation project(":apng_android")
implementation project(":anko")
implementation fileTree(include: ["*.aar"], dir: "src/main/libs")
// implementation "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.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")
@ -189,6 +199,29 @@ repositories {
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) {
description = "Custom DETEKT build for all modules"
@ -247,3 +280,4 @@ tasks.register("detektAll", Detekt) {
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
android:name=".App1"
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_spec"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:maxAspectRatio="100"
android:resizeableActivity="true"
android:supportsRtl="true"
android:theme="@style/AppTheme.Light"
tools:ignore="DataExtractionRules,UnusedAttribute">
<!-- android:localeConfig="@xml/locales_config" -->
<activity
android:name=".ActMain"
@ -362,23 +363,38 @@
</intent-filter>
</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
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<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" />
</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>
</manifest>

View File

@ -2,6 +2,7 @@ package jp.juggler.subwaytooter
import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
@ -15,24 +16,29 @@ import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.action.accountRemove
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.auth.AuthRepo
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.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.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.*
import jp.juggler.util.*
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.coroutine.launchProgress
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption
import jp.juggler.util.media.ResizeConfig
import jp.juggler.util.media.ResizeType
import jp.juggler.util.media.createResizedBitmap
@ -124,6 +130,10 @@ class ActAccountSetting : AppCompatActivity(),
ActAccountSettingBinding.inflate(layoutInflater, null, false)
}
private val authRepo by lazy {
AuthRepo(this)
}
private lateinit var nameInvalidator: NetworkEmojiInvalidator
private lateinit var noteInvalidator: NetworkEmojiInvalidator
private lateinit var defaultTextInvalidator: NetworkEmojiInvalidator
@ -218,20 +228,22 @@ class ActAccountSetting : AppCompatActivity(),
initUI()
val a = intent.long(KEY_ACCOUNT_DB_ID)
?.let { SavedAccount.loadAccount(this, it) }
if (a == null) {
finish()
return
launchAndShowError {
val a = intent.long(KEY_ACCOUNT_DB_ID)
?.let { daoSavedAccount.loadAccount(it) }
if (a == null) {
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) {
@ -336,7 +348,7 @@ class ActAccountSetting : AppCompatActivity(),
R.id.etFieldValue4
).map { findViewById(it) }
btnNotificationStyleEditReply.vg(PrefB.bpSeparateReplyNotificationGroup.invoke())
btnNotificationStyleEditReply.vg(PrefB.bpSeparateReplyNotificationGroup.value)
nameInvalidator = NetworkEmojiInvalidator(handler, etDisplayName)
noteInvalidator = NetworkEmojiInvalidator(handler, etNote)
@ -504,72 +516,76 @@ class ActAccountSetting : AppCompatActivity(),
}
private fun showAcctColor() {
val sa = this.account
val ac = AcctColor.load(sa)
val ac = daoAcctColor.load(sa)
views.tvUserCustom.apply {
backgroundColor = ac.color_bg
backgroundColor = ac.colorBg
text = ac.nickname
textColor = ac.color_fg.notZero() ?: attrColor(R.attr.colorTimeSmall)
textColor = ac.colorFg.notZero()
?: attrColor(R.attr.colorTimeSmall)
}
}
private fun saveUIToData() {
if (!::account.isInitialized) return
if (loadingBusy) return
account.visibility = visibility
launchAndShowError {
views.apply {
account.visibility = visibility
account.dont_hide_nsfw = swNSFWOpen.isChecked
account.dont_show_timeout = swDontShowTimeout.isChecked
account.expand_cw = swExpandCW.isChecked
account.default_sensitive = swMarkSensitive.isChecked
account.notification_mention = cbNotificationMention.isChecked
account.notification_boost = cbNotificationBoost.isChecked
account.notification_favourite = cbNotificationFavourite.isChecked
account.notification_follow = cbNotificationFollow.isChecked
account.notification_follow_request = cbNotificationFollowRequest.isChecked
account.notification_reaction = cbNotificationReaction.isChecked
account.notification_vote = cbNotificationVote.isChecked
account.notification_post = cbNotificationPost.isChecked
account.notification_update = cbNotificationUpdate.isChecked
account.notification_status_reference = cbNotificationStatusReference.isChecked
views.apply {
account.dont_hide_nsfw = swNSFWOpen.isChecked
account.dont_show_timeout = swDontShowTimeout.isChecked
account.expand_cw = swExpandCW.isChecked
account.default_sensitive = swMarkSensitive.isChecked
account.notification_mention = cbNotificationMention.isChecked
account.notification_boost = cbNotificationBoost.isChecked
account.notification_favourite = cbNotificationFavourite.isChecked
account.notification_follow = cbNotificationFollow.isChecked
account.notification_follow_request = cbNotificationFollowRequest.isChecked
account.notification_reaction = cbNotificationReaction.isChecked
account.notification_vote = cbNotificationVote.isChecked
account.notification_post = cbNotificationPost.isChecked
account.notification_update = cbNotificationUpdate.isChecked
account.notification_status_reference = cbNotificationStatusReference.isChecked
account.confirm_follow = cbConfirmFollow.isChecked
account.confirm_follow_locked = cbConfirmFollowLockedUser.isChecked
account.confirm_unfollow = cbConfirmUnfollow.isChecked
account.confirm_boost = cbConfirmBoost.isChecked
account.confirm_favourite = cbConfirmFavourite.isChecked
account.confirm_unboost = cbConfirmUnboost.isChecked
account.confirm_unfavourite = cbConfirmUnfavourite.isChecked
account.confirm_post = cbConfirmToot.isChecked
account.confirm_reaction = cbConfirmReaction.isChecked
account.confirm_unbookmark = cbConfirmUnbookmark.isChecked
account.confirm_follow = cbConfirmFollow.isChecked
account.confirm_follow_locked = cbConfirmFollowLockedUser.isChecked
account.confirm_unfollow = cbConfirmUnfollow.isChecked
account.confirm_boost = cbConfirmBoost.isChecked
account.confirm_favourite = cbConfirmFavourite.isChecked
account.confirm_unboost = cbConfirmUnboost.isChecked
account.confirm_unfavourite = cbConfirmUnfavourite.isChecked
account.confirm_post = cbConfirmToot.isChecked
account.confirm_reaction = cbConfirmReaction.isChecked
account.confirm_unbookmark = cbConfirmUnbookmark.isChecked
account.sound_uri = ""
account.default_text = etDefaultText.text.toString()
account.sound_uri = ""
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.image_max_megabytes = etMediaSizeMax.text.toString().trim()
account.image_resize = (
imageResizeItems.elementAtOrNull(spResizeImage.selectedItemPosition)?.config
?: SavedAccount.defaultResizeConfig
).spec
account.movie_max_megabytes = etMovieSizeMax.text.toString().trim()
account.image_max_megabytes = etMediaSizeMax.text.toString().trim()
account.image_resize = (
imageResizeItems.elementAtOrNull(spResizeImage.selectedItemPosition)?.config
?: SavedAccount.defaultResizeConfig
).spec
account.push_policy =
pushPolicyItems.elementAtOrNull(spPushPolicy.selectedItemPosition)?.id
account.push_policy =
pushPolicyItems.elementAtOrNull(spPushPolicy.selectedItemPosition)?.id
account.movieTranscodeMode = spMovieTranscodeMode.selectedItemPosition
account.movieTranscodeBitrate = etMovieBitrate.text.toString()
account.movieTranscodeFramerate = etMovieFrameRate.text.toString()
account.movieTranscodeSquarePixels = etMovieSquarePixels.text.toString()
account.lang = languages.elementAtOrNull(spLanguageCode.selectedItemPosition)?.first
?: SavedAccount.LANG_WEB
account.movieTranscodeMode = spMovieTranscodeMode.selectedItemPosition
account.movieTranscodeBitrate = etMovieBitrate.text.toString()
account.movieTranscodeFramerate = etMovieFrameRate.text.toString()
account.movieTranscodeSquarePixels = etMovieSquarePixels.text.toString()
account.lang = languages.elementAtOrNull(spLanguageCode.selectedItemPosition)?.first
?: SavedAccount.LANG_WEB
}
daoSavedAccount.saveSetting(account)
}
account.saveSetting()
}
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
@ -617,24 +633,20 @@ class ActAccountSetting : AppCompatActivity(),
R.id.btnFields -> sendFields()
R.id.btnNotificationStyleEdit ->
MessageNotification.openNotificationChannelSetting(
this,
account,
MessageNotification.TRACKING_NAME_DEFAULT
PullNotification.openNotificationChannelSetting(
this
)
R.id.btnNotificationStyleEditReply ->
MessageNotification.openNotificationChannelSetting(
this,
account,
MessageNotification.TRACKING_NAME_REPLY
PullNotification.openNotificationChannelSetting(
this
)
}
}
private fun showVisibility() {
views.btnVisibility.text =
getVisibilityString(this, account.isMisskey, visibility)
visibility.getVisibilityString(account.isMisskey)
}
private fun performVisibility() {
@ -734,10 +746,11 @@ class ActAccountSetting : AppCompatActivity(),
.setMessage(R.string.confirm_account_remove)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
accountRemove(account)
finish()
}
.show()
launchAndShowError {
authRepo.accountRemove(account)
finish()
}
}.show()
}
///////////////////////////////////////////////////
@ -823,7 +836,7 @@ class ActAccountSetting : AppCompatActivity(),
result.jsonObject
} else {
// 承認待ち状態のチェック
account.checkConfirmed(this, client)
authRepo.checkConfirmed(account, client)
val result = client.request(
"/api/v1/accounts/verify_credentials"
@ -1259,21 +1272,21 @@ class ActAccountSetting : AppCompatActivity(),
}
private fun openPicker(permissionRequester: PermissionRequester) {
if (!permissionRequester.checkOrLaunch()) return
val propName = when (permissionRequester) {
prPickHeader -> "header"
else -> "avatar"
launchAndShowError {
if (!permissionRequester.checkOrLaunch()) return@launchAndShowError
val propName = when (permissionRequester) {
prPickHeader -> "header"
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) {
@ -1416,19 +1429,57 @@ class ActAccountSetting : AppCompatActivity(),
}
private fun updatePushSubscription(force: Boolean) {
val wps = PushSubscriptionHelper(applicationContext, account, verbose = true)
launchMain {
runApiTask(account) { client ->
wps.updateSubscription(client, force = force)
}?.let {
val log = wps.logString
if (log.isNotEmpty()) {
AlertDialog.Builder(this@ActAccountSetting)
.setMessage(log)
.setPositiveButton(R.string.close, null)
.show()
val activity = this
launchAndShowError {
val anyNotificationWanted = account.notification_boost ||
account.notification_favourite ||
account.notification_follow ||
account.notification_mention ||
account.notification_reaction ||
account.notification_vote ||
account.notification_follow_request ||
account.notification_post ||
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
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ResolveInfo
import android.graphics.Color
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.SettingType
import jp.juggler.subwaytooter.appsetting.appSettingRoot
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.databinding.ActAppSettingBinding
import jp.juggler.subwaytooter.databinding.LvSettingItemBinding
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.IntPref
import jp.juggler.subwaytooter.pref.impl.StringPref
import jp.juggler.subwaytooter.pref.pref
import jp.juggler.subwaytooter.pref.put
import jp.juggler.subwaytooter.pref.remove
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.pref.lazyPref
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.cn
@ -92,7 +90,6 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
private var customShareTarget: CustomShareTarget? = null
lateinit var pref: SharedPreferences
lateinit var handler: Handler
val views by lazy {
@ -103,6 +100,10 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
MyAdapter()
}
val authRepo by lazy {
AuthRepo(this)
}
private val arNoop = ActivityResultHandler(log) { }
private val arImportAppData = ActivityResultHandler(log) { r ->
@ -159,7 +160,6 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
App1.setActivityTheme(this)
this.handler = App1.getAppState(this).handler
this.pref = pref()
// val intent = this.intent
// val layoutId = intent.getIntExtra(EXTRA_LAYOUT_ID, 0)
@ -218,12 +218,12 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
}
private fun removeDefaultPref() {
val e = pref.edit()
val e = lazyPref.edit()
var changed = false
appSettingRoot.scan {
when {
(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()
@ -371,7 +371,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
SettingType.ColorAlpha -> newColor.notZero() ?: 1
else -> newColor or Color.BLACK
}
pref.edit().put(ip, c).apply()
ip.value = c
findItemViewHolder(colorTarget)?.showColor()
colorTarget.changed(this)
}
@ -512,8 +512,6 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
private val tvDesc = views.tvDesc
private val tvError = views.tvError
private val pref = actAppSetting.pref
var item: AppSettingItem? = null
private var bindingBusy = false
@ -575,7 +573,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
vg(false) // skip animation
text = name
isEnabledAlpha = item.enabled
isChecked = bp(pref)
isChecked = bp.value
vg(true)
}
@ -586,7 +584,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
vg(false) // skip animation
actAppSetting.setSwitchColor(views.swSwitch)
isEnabledAlpha = item.enabled
isChecked = bp(pref)
isChecked = bp.value
vg(true)
}
@ -608,12 +606,12 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
showCaption(name)
views.llButtonBar.vg(true)
views.vColor.vg(true)
views.vColor.setBackgroundColor(ip(pref))
views.vColor.setBackgroundColor(ip.value)
views.btnEdit.isEnabledAlpha = item.enabled
views.btnReset.isEnabledAlpha = item.enabled
views.btnEdit.setOnClickListener {
actAppSetting.colorTarget = item
val color = ip(pref)
val color = ip.value
val builder = ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
@ -623,7 +621,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
builder.show(actAppSetting)
}
views.btnReset.setOnClickListener {
pref.edit().remove(ip).apply()
ip.removeValue()
showColor()
item.changed.invoke(actAppSetting)
}
@ -644,7 +642,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
argsInt?.map { actAppSetting.getString(it) }
?: item.spinnerArgsProc(actAppSetting)
)
views.spSpinner.setSelection(pi.invoke(pref))
views.spSpinner.setSelection(pi.value)
} else {
item.spinnerInitializer.invoke(actAppSetting, views.spSpinner)
}
@ -655,9 +653,9 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
views.etEditText.vg(true)?.let { etEditText ->
val text = when (val pi = item.pref) {
is FloatPref ->
item.fromFloat.invoke(actAppSetting, pi(pref))
item.fromFloat.invoke(actAppSetting, pi.value)
is StringPref ->
pi(pref)
pi.value
else -> error("EditText has incorrect pref $pi")
}
@ -736,7 +734,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
fun showColor() {
val item = item ?: return
val ip = item.pref.cast<IntPref>() ?: return
val c = ip(pref)
val c = ip.value
views.vColor.setBackgroundColor(c)
}
@ -753,15 +751,14 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
val sv = item.filter.invoke(p0?.toString() ?: "")
when (val pi = item.pref) {
is StringPref ->
pref.edit().put(pi, sv).apply()
is StringPref -> pi.value = sv
is FloatPref -> {
val fv = item.toFloat.invoke(actAppSetting, sv)
if (fv.isFinite()) {
pref.edit().put(pi, fv).apply()
pi.value = fv
} else {
pref.edit().remove(pi.key).apply()
pi.removeValue()
}
}
@ -785,7 +782,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
if (bindingBusy) return
val item = item ?: return
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)
}
item.changed.invoke(actAppSetting)
@ -795,7 +792,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
if (bindingBusy) return
val item = item ?: return
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")
}
item.changed.invoke(actAppSetting)
@ -946,7 +943,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
val file = saveTimelineFont(it, fileName)
if (file != null) {
pref.edit().put(item.pref.cast()!!, file.absolutePath).apply()
(item.pref as? StringPref)?.value = file.absolutePath
showTimelineFont(item)
}
}
@ -959,19 +956,17 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
}
fun showTimelineFont(item: AppSettingItem, tv: TextView) {
val fontUrl = item.pref.cast<StringPref>()!!.invoke(this)
try {
if (fontUrl.isNotEmpty()) {
item.pref.cast<StringPref>()?.value.notEmpty()?.let { url ->
tv.typeface = Typeface.DEFAULT
val face = Typeface.createFromFile(fontUrl)
val face = Typeface.createFromFile(url)
tv.typeface = face
tv.text = fontUrl
tv.text = url
return
}
} catch (ex: Throwable) {
log.e(ex, "showTimelineFont failed.")
}
// fallback
tv.text = getString(R.string.not_selected)
tv.typeface = Typeface.DEFAULT
@ -1026,17 +1021,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
//////////////////////////////////////////////////////
inner class AccountAdapter internal constructor() : BaseAdapter() {
internal val list = ArrayList<SavedAccount>()
init {
for (a in SavedAccount.loadAccountList(this@ActAppSetting)) {
if (a.isPseudo) continue
list.add(a)
}
SavedAccount.sort(list)
}
inner class AccountAdapter(val list: List<SavedAccount>) : BaseAdapter() {
override fun getCount(): Int {
return 1 + list.size
@ -1058,7 +1043,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
)
view.findViewById<TextView>(android.R.id.text1).text = when (position) {
0 -> getString(R.string.ask_always)
else -> AcctColor.getNickname(list[position - 1])
else -> daoAcctColor.getNickname(list[position - 1])
}
return view
}
@ -1068,7 +1053,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
viewOld ?: layoutInflater.inflate(R.layout.lv_spinner_dropdown, parent, false)
view.findViewById<TextView>(android.R.id.text1).text = when (position) {
0 -> getString(R.string.ask_always)
else -> AcctColor.getNickname(list[position - 1])
else -> daoAcctColor.getNickname(list[position - 1])
}
return view
}
@ -1201,7 +1186,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
fun setCustomShare(appSettingItem: AppSettingItem, target: CustomShareTarget, value: String) {
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)
}
@ -1238,7 +1223,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
private fun setWebBrowser(appSettingItem: AppSettingItem, value: String) {
val sp: StringPref = appSettingItem.pref.cast()
?: error("${getString(appSettingItem.caption)}: not StringPref")
pref.edit().put(sp, value).apply()
sp.value = 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.LvMuteAppBinding
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.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.setNavigationBack
import kotlinx.coroutines.withContext
class ActFavMute : AppCompatActivity() {
@ -52,27 +54,20 @@ class ActFavMute : AppCompatActivity() {
item ?: return
launchAndShowError {
confirm(R.string.delete_confirm, item.acct.pretty)
FavMute.delete(item.acct)
daoFavMute.delete(item.acct)
listAdapter.remove(item)
}
}
private fun loadData() {
listAdapter.items = buildList {
try {
FavMute.createCursor().use { cursor ->
val idxId = cursor.getColumnIndex(FavMute.COL_ID)
val idxName = cursor.getColumnIndex(FavMute.COL_ACCT)
while (cursor.moveToNext()) {
val item = MyItem(
id = cursor.getLong(idxId),
acct = Acct.parse(cursor.getString(idxName)),
)
add(item)
}
launchAndShowError {
listAdapter.items = withContext(AppDispatchers.IO) {
daoFavMute.listAll().map {
MyItem(
id = it.id,
acct = Acct.parse(it.acct),
)
}
} 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 jp.juggler.subwaytooter.databinding.ActHighlightEditBinding
import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.subwaytooter.table.daoHighlightWord
import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.decodeJsonObject
import jp.juggler.util.data.mayUri
import jp.juggler.util.data.notEmpty
@ -83,32 +85,34 @@ class ActHighlightWordEdit
setResult(RESULT_CANCELED)
fun loadData(): HighlightWord? {
savedInstanceState?.getString(STATE_ITEM)
?.decodeJsonObject()
?.let { return HighlightWord(it) }
launchAndShowError {
fun loadData(): HighlightWord? {
savedInstanceState?.getString(STATE_ITEM)
?.decodeJsonObject()
?.let { return HighlightWord(it) }
intent?.string(EXTRA_INITIAL_TEXT)
?.let { return HighlightWord(it) }
intent?.string(EXTRA_INITIAL_TEXT)
?.let { return HighlightWord(it) }
intent?.long(EXTRA_ITEM_ID)
?.let { return HighlightWord.load(it) }
intent?.long(EXTRA_ITEM_ID)
?.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) {
@ -254,22 +258,26 @@ class ActHighlightWordEdit
}
private fun save() {
uiToData()
launchAndShowError {
uiToData()
val name = item.name
if (item.name.isEmpty()) {
showToast(true, R.string.cant_leave_empty_keyword)
return
if (name.isNullOrBlank()) {
showToast(true, R.string.cant_leave_empty_keyword)
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.dialog.DlgConfirm.confirm
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.data.cast
import jp.juggler.util.data.mayUri
import jp.juggler.util.data.notBlank
import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.errorEx
import jp.juggler.util.ui.*
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference
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 {
uri?.let { RingtoneManager.getRingtone(context, it) }
?.let { ringtone ->
@ -129,21 +132,10 @@ class ActHighlightWordList : AppCompatActivity() {
}
private fun loadData() {
try {
listAdapter.items = buildList {
HighlightWord.createCursor().use { cursor ->
val colIdx = HighlightWord.ColIdx(cursor)
while (cursor.moveToNext()) {
try {
add(HighlightWord(cursor, colIdx))
} catch (ex: Throwable) {
log.e(ex, "load error.")
}
}
}
launchAndShowError {
listAdapter.items = withContext(AppDispatchers.IO) {
daoHighlightWord.listAll()
}
} catch (ex: Throwable) {
errorEx(ex, "query error.")
}
}
@ -161,15 +153,17 @@ class ActHighlightWordList : AppCompatActivity() {
val activity = this
launchAndShowError {
confirm(getString(R.string.delete_confirm, item.name))
item.delete(activity)
daoHighlightWord.delete(applicationContext, item)
listAdapter.remove(item)
App1.getAppState(activity).enableSpeech()
}
}
private fun speech(item: HighlightWord?) {
item ?: return
App1.getAppState(this@ActHighlightWordList)
.addSpeech(item.name, dedupMode = DedupMode.None)
item?.name?.notBlank()?.let {
App1.getAppState(this@ActHighlightWordList)
.addSpeech(it, dedupMode = DedupMode.None)
}
}
// リスト要素のViewHolder

View File

@ -12,12 +12,15 @@ import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.databinding.ActKeywordFilterBinding
import jp.juggler.subwaytooter.databinding.LvKeywordFilterBinding
import jp.juggler.subwaytooter.table.AcctColor
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.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.*
import jp.juggler.util.int
@ -96,59 +99,64 @@ class ActKeywordFilter : AppCompatActivity() {
private val deleteIds = HashSet<String>()
val authRepo by lazy {
AuthRepo(this)
}
///////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
backPressed { confirmBack() }
super.onCreate(savedInstanceState)
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()
showAccount()
launchAndShowError {
if (savedInstanceState == null) {
if (filterId != null) {
startLoading()
} else {
views.spExpire.setSelection(1)
val initialText = intent.string(EXTRA_INITIAL_PHRASE)?.trim() ?: ""
views.etTitle.setText(initialText)
addKeywordArea(TootFilterKeyword(keyword = initialText))
// filter ID の有無はUIに影響するのでinitUIより先に初期化する
filterId = EntityId.from(intent, EXTRA_FILTER_ID)
val a = intent.long(EXTRA_ACCOUNT_DB_ID)
?.let { daoSavedAccount.loadAccount(it) }
if (a == null) {
finish()
return@launchAndShowError
}
} else {
account = a
savedInstanceState.getStringArrayList(STATE_DELETE_IDS)
?.let { deleteIds.addAll(it) }
savedInstanceState.getStringArrayList(STATE_KEYWORDS)
?.mapNotNull { it?.decodeJsonObject() }
?.forEach {
try {
addKeywordArea(TootFilterKeyword(it))
} catch (ex: Throwable) {
log.e(ex, "can't decode TootFilterKeyword")
}
showAccount()
if (savedInstanceState == null) {
if (filterId != null) {
startLoading()
} else {
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)
?.let { views.spExpire.setSelection(it) }
savedInstanceState.getStringArrayList(STATE_DELETE_IDS)
?.let { deleteIds.addAll(it) }
savedInstanceState.long(STATE_EXPIRE_AT)
?.let { filterExpire = it }
savedInstanceState.getStringArrayList(STATE_KEYWORDS)
?.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() {
views.tvAccount.text = AcctColor.getNicknameWithColor(account.acct)
views.tvAccount.text = daoAcctColor.getNicknameWithColor(account.acct)
}
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.column.Column
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.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchProgress
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
@ -263,15 +264,17 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
R.id.btnAdd -> edit(null)
R.id.btnMore -> {
ActionsDialog()
.addAction(getString(R.string.clear_all)) {
languageList.clear()
languageList.add(MyItem(TootStatus.LANGUAGE_CODE_DEFAULT, true))
adapter.notifyDataSetChanged()
launchAndShowError {
actionsDialog {
action(getString(R.string.clear_all)) {
languageList.clear()
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 =
activity.languageNameMap.map { MyItem(it.key, true) }.sortedWith(languageComparator)
btnPresets.setOnClickListener {
val ad = ActionsDialog()
for (a in languageList) {
ad.addAction("${a.code} ${activity.getDesc(a)}") {
etLanguage.setText(a.code)
updateDesc()
activity.run {
launchAndShowError {
actionsDialog(getString(R.string.presets)) {
for (a in languageList) {
action("${a.code} ${activity.getDesc(a)}") {
etLanguage.setText(a.code)
updateDesc()
}
}
}
}
}
ad.show(activity, activity.getString(R.string.presets))
}
etLanguage.setText(item?.code ?: "")

View File

@ -3,7 +3,6 @@ package jp.juggler.subwaytooter
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Typeface
import android.os.Build
@ -19,6 +18,7 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
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.itemviewholder.StatusButtonsPopup
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.pref.put
import jp.juggler.subwaytooter.pref.*
import jp.juggler.subwaytooter.span.MyClickableSpan
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.view.MyDrawerLayout
import jp.juggler.subwaytooter.view.MyEditText
import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.notEmpty
import jp.juggler.util.int
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.attrColor
import jp.juggler.util.ui.isNotOk
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.internal.toHexString
import java.lang.ref.WeakReference
import java.util.*
@ -152,7 +153,6 @@ class ActMain : AppCompatActivity(),
lateinit var completionHelper: CompletionHelper
lateinit var pref: SharedPreferences
lateinit var handler: Handler
lateinit var appState: AppState
@ -196,7 +196,7 @@ class ActMain : AppCompatActivity(),
override fun run() {
handler.removeCallbacks(this)
if (!isStartedEx) return
if (PrefB.bpRelativeTimestamp(pref)) {
if (PrefB.bpRelativeTimestamp.value) {
appState.columnList.forEach { it.fireRelativeTime() }
handler.postDelayed(this, 10000L)
}
@ -209,7 +209,7 @@ class ActMain : AppCompatActivity(),
set(value) {
if (value != quickPostVisibility) {
quickPostVisibility = value
pref.edit().put(PrefS.spQuickTootVisibility, value.id.toString()).apply()
PrefS.spQuickTootVisibility.value = value.id.toString()
showQuickPostVisibility()
}
}
@ -262,7 +262,7 @@ class ActMain : AppCompatActivity(),
}
val arAppSetting = ActivityResultHandler(log) { r ->
Column.reloadDefaultColor(this, pref)
Column.reloadDefaultColor(this)
showFooterColor()
updateColumnStrip()
if (r.resultCode == RESULT_APP_DATA_IMPORT) {
@ -282,15 +282,17 @@ class ActMain : AppCompatActivity(),
}
val arAccountSetting = ActivityResultHandler(log) { r ->
updateColumnStrip()
appState.columnList.forEach { it.fireShowColumnHeader() }
when (r.resultCode) {
RESULT_OK -> r.data?.data?.let { openBrowser(it) }
launchAndShowError {
updateColumnStrip()
appState.columnList.forEach { it.fireShowColumnHeader() }
when (r.resultCode) {
RESULT_OK -> r.data?.data?.let { openBrowser(it) }
ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN ->
r.data?.long(ActAccountSetting.EXTRA_DB_ID)
?.let { SavedAccount.loadAccount(this, it) }
?.let { accessTokenPrompt(it.apiHost) }
ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN ->
r.data?.long(ActAccountSetting.EXTRA_DB_ID)
?.let { daoSavedAccount.loadAccount(it) }
?.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)
handler = appState.handler
pref = appState.pref
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()
reloadTextSize()
@ -373,8 +376,6 @@ class ActMain : AppCompatActivity(),
if (savedInstanceState != null) {
sharedIntent2?.let { handleSharedIntent(it) }
}
checkPrivacyPolicy()
}
override fun onDestroy() {
@ -452,91 +453,94 @@ class ActMain : AppCompatActivity(),
sideMenuAdapter.onActivityStart()
launchDialogs()
// 残りの処理はActivityResultの処理より後回しにしたい
handler.postDelayed(onStartAfter, 1L)
prNotification.checkOrLaunch()
themeDefaultChangedDialog()
}
}
private val onStartAfter = Runnable {
benchmark("onStartAfter total") {
benchmark("sweepBuggieData") {
// バグいアカウントデータを消す
lifecycleScope.launch {
try {
SavedAccount.sweepBuggieData()
} catch (ex: Throwable) {
log.e(ex, "sweepBuggieData failed.")
}
}
delay(1L)
benchmark("onStartAfter total") {
val newAccounts = benchmark("loadAccountList") {
SavedAccount.loadAccountList(this)
}
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
benchmark("sweepBuggieData") {
// バグいアカウントデータを消す
try {
daoSavedAccount.sweepBuggieData()
} catch (ex: Throwable) {
log.e(ex, "sweepBuggieData failed.")
}
}
}.takeIf { it.size != appState.columnCount }
?.let { setColumnsOrder(it) }
}
benchmark("fireColumnColor") {
// 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす
appState.columnList.forEach { column ->
column.viewHolder?.lastAnnouncementShown = 0L
column.fireColumnColor()
val newAccounts = benchmark("loadAccountList") {
daoSavedAccount.loadAccountList()
}
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.")
}
}
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()
}
}.let { startAfterJob = WeakReference(it) }
}
}
override fun onStop() {
log.d("onStop")
isStartedEx = false
handler.removeCallbacks(onStartAfter)
startAfterJob?.get()?.cancel()
startAfterJob = null
handler.removeCallbacks(procUpdateRelativeTime)
completionHelper.closeAcctPopup()
@ -583,7 +587,7 @@ class ActMain : AppCompatActivity(),
at android.os.Binder.execTransact (Binder.java:739)
*/
if (PrefB.bpDontScreenOff(pref)) {
if (PrefB.bpDontScreenOff.value) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@ -611,7 +615,7 @@ class ActMain : AppCompatActivity(),
{ env -> env.pager.currentItem },
{ env -> env.visibleColumnsIndices.first })
log.d("ipLastColumnPos save $lastPos")
pref.edit().put(PrefI.ipLastColumnPos, lastPos).apply()
PrefI.ipLastColumnPos.value = lastPos
appState.columnList.forEach { it.saveScrollPosition() }
@ -701,10 +705,10 @@ class ActMain : AppCompatActivity(),
setContentView(R.layout.act_main)
quickPostVisibility =
TootVisibility.parseSavedVisibility(PrefS.spQuickTootVisibility(pref))
TootVisibility.parseSavedVisibility(PrefS.spQuickTootVisibility.value)
?: quickPostVisibility
Column.reloadDefaultColor(this, pref)
Column.reloadDefaultColor(this)
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.entity.*
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.global.appPref
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.put
import jp.juggler.subwaytooter.util.permissionSpecMediaDownload
import jp.juggler.subwaytooter.util.requester
import jp.juggler.subwaytooter.view.PinchBitmapView
import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
@ -304,7 +303,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
views.pbvImage.background = MediaBackgroundDrawable(
context = views.root.context,
tileStep = tileStep,
kind = MediaBackgroundDrawable.Kind.fromIndex(PrefI.ipMediaBackground(this))
kind = MediaBackgroundDrawable.Kind.fromIndex(PrefI.ipMediaBackground.value)
)
val enablePaging = mediaList.size > 1
@ -444,7 +443,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
val url = when {
forceLocalUrl -> ta.url
else -> ta.getLargeUrl(appPref)
else -> ta.getLargeUrl()
}
if (url == null) {
showError("missing media attachment url.")
@ -573,7 +572,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
pbvImage.visible().setBitmap(null)
}
val urlList = ta.getLargeUrlList(appPref)
val urlList = ta.getLargeUrlList()
if (urlList.isEmpty()) {
showError("missing media attachment url.")
return
@ -650,7 +649,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
?: error("missing DownloadManager system service")
val url = if (ta is TootAttachment) {
ta.getLargeUrl(appPref)
ta.getLargeUrl()
} else {
null
} ?: return
@ -781,65 +780,80 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
}
private fun more(ta: TootAttachmentLike) {
val ad = ActionsDialog()
if (ta is TootAttachment) {
val url = ta.getLargeUrl(appPref) ?: return
ad.addAction(getString(R.string.open_in_browser)) { share(Intent.ACTION_VIEW, url) }
ad.addAction(getString(R.string.share_url)) { share(Intent.ACTION_SEND, url) }
ad.addAction(getString(R.string.copy_url)) { copy(url) }
addMoreMenu(ad, "url", ta.url, Intent.ACTION_VIEW)
addMoreMenu(ad, "remote_url", ta.remote_url, Intent.ACTION_VIEW)
addMoreMenu(ad, "preview_url", ta.preview_url, Intent.ACTION_VIEW)
addMoreMenu(ad, "preview_remote_url", ta.preview_remote_url, Intent.ACTION_VIEW)
addMoreMenu(ad, "text_url", ta.text_url, Intent.ACTION_VIEW)
} else if (ta is TootAttachmentMSP) {
val url = ta.preview_url
ad.addAction(getString(R.string.open_in_browser)) { share(Intent.ACTION_VIEW, url) }
ad.addAction(getString(R.string.share_url)) { share(Intent.ACTION_SEND, url) }
ad.addAction(getString(R.string.copy_url)) { copy(url) }
}
launchAndShowError {
actionsDialog {
fun addMoreMenu(
captionPrefix: String,
url: String?,
@Suppress("SameParameterValue") action: String,
) {
val uri = url.mayUri() ?: return
val caption = getString(R.string.open_browser_of, captionPrefix)
action(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.")
}
}
}
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) {
ad.addAction(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.")
if (TootAttachmentType.Image == mediaList.elementAtOrNull(idx)?.type) {
action(getString(R.string.background_pattern)) { mediaBackgroundDialog() }
}
}
}
}
private fun mediaBackgroundDialog() {
val ad = ActionsDialog()
for (k in MediaBackgroundDrawable.Kind.values()) {
if (!k.isMediaBackground) continue
ad.addAction(k.name) {
val idx = k.toIndex()
appPref.edit().put(PrefI.ipMediaBackground, idx).apply()
views.pbvImage.background = MediaBackgroundDrawable(
context = views.root.context,
tileStep = tileStep,
kind = k
)
launchAndShowError {
actionsDialog(getString(R.string.background_pattern)) {
for (k in MediaBackgroundDrawable.Kind.values()) {
if (!k.isMediaBackground) continue
action(k.name) {
val idx = k.toIndex()
PrefI.ipMediaBackground.value = idx
views.pbvImage.background = MediaBackgroundDrawable(
context = views.root.context,
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.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.MutedApp
import jp.juggler.subwaytooter.table.appDatabase
import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.setNavigationBack
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ActMutedApp : AppCompatActivity() {
@ -27,6 +30,8 @@ class ActMutedApp : AppCompatActivity() {
private val listAdapter by lazy { MyListAdapter() }
private val daoMutedApp by lazy { MutedApp.Access(appDatabase) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
backPressed {
@ -47,48 +52,33 @@ class ActMutedApp : AppCompatActivity() {
}
private fun loadData() {
listAdapter.items = buildList {
try {
MutedApp.createCursor().use { cursor ->
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.")
launchAndShowError {
listAdapter.items = withContext(Dispatchers.IO) {
daoMutedApp.listAll()
}
}
}
private fun delete(item: MyItem?) {
private fun delete(item: MutedApp?) {
item ?: return
launchAndShowError {
confirm(R.string.delete_confirm, item.name)
MutedApp.delete(item.name)
daoMutedApp.delete(item.name)
listAdapter.remove(item)
}
}
// リスト要素のデータ
private class MyItem(val id: Long, val name: String)
// リスト要素のViewHolder
private inner class MyViewHolder(parent: ViewGroup?) {
val views = LvMuteAppBinding.inflate(layoutInflater, parent, false)
var lastItem: MyItem? = null
var lastItem: MutedApp? = null
init {
views.root.tag = this
views.btnDelete.setOnClickListener { delete(lastItem) }
}
fun bind(item: MyItem?) {
fun bind(item: MutedApp?) {
item ?: return
lastItem = item
views.tvName.text = item.name
@ -96,13 +86,13 @@ class ActMutedApp : AppCompatActivity() {
}
private inner class MyListAdapter : BaseAdapter() {
var items: List<MyItem> = emptyList()
var items: List<MutedApp> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}
fun remove(item: MyItem) {
fun remove(item: MutedApp) {
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.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.setNavigationBack
import kotlinx.coroutines.withContext
class ActMutedPseudoAccount : AppCompatActivity() {
@ -47,62 +50,47 @@ class ActMutedPseudoAccount : AppCompatActivity() {
}
private fun loadData() {
listAdapter.items = buildList {
try {
UserRelation.createCursorPseudoMuted().use { cursor ->
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.")
launchAndShowError {
listAdapter.items = withContext(AppDispatchers.IO) {
daoUserRelation.listPseudoMuted()
}
}
}
private fun delete(item: MyItem?) {
private fun delete(item: UserRelation?) {
item ?: return
launchAndShowError {
confirm(R.string.delete_confirm, item.name)
UserRelation.deletePseudo(item.id)
confirm(R.string.delete_confirm, item.whoId)
daoUserRelation.deletePseudo(item.id)
listAdapter.remove(item)
}
}
// リスト要素のデータ
private class MyItem(val id: Long, val name: String)
// リスト要素のViewHolder
private inner class MyViewHolder(parent: ViewGroup?) {
val views = LvMuteAppBinding.inflate(layoutInflater, parent, false)
private var lastItem: MyItem? = null
private var lastItem: UserRelation? = null
init {
views.root.tag = this
views.btnDelete.setOnClickListener { delete(lastItem) }
}
fun bind(item: MyItem?) {
fun bind(item: UserRelation?) {
item ?: return
lastItem = item
views.tvName.text = item.name
views.tvName.text = item.whoId
}
}
private inner class MyListAdapter : BaseAdapter() {
var items: List<MyItem> = emptyList()
var items: List<UserRelation> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}
fun remove(item: MyItem) {
fun remove(item: UserRelation) {
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.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.MutedWord
import jp.juggler.subwaytooter.table.daoMutedWord
import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.setNavigationBack
import kotlinx.coroutines.withContext
class ActMutedWord : AppCompatActivity() {
@ -47,48 +50,33 @@ class ActMutedWord : AppCompatActivity() {
}
private fun loadData() {
listAdapter.items = buildList {
try {
MutedWord.createCursor().use { cursor ->
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.")
launchAndShowError {
listAdapter.items = withContext(AppDispatchers.IO) {
daoMutedWord.listAll()
}
}
}
private fun delete(item: MyItem?) {
private fun delete(item: MutedWord?) {
item ?: return
launchAndShowError {
confirm(R.string.delete_confirm, item.name)
MutedWord.delete(item.name)
daoMutedWord.delete(item.name)
listAdapter.remove(item)
}
}
// リスト要素のデータ
private class MyItem(val id: Long, val name: String)
// リスト要素のViewHolder
private inner class MyViewHolder(parent: ViewGroup?) {
val views = LvMuteAppBinding.inflate(layoutInflater, parent, false)
private var lastItem: MyItem? = null
private var lastItem: MutedWord? = null
init {
views.root.tag = this
views.btnDelete.setOnClickListener { delete(lastItem) }
}
fun bind(item: MyItem?) {
fun bind(item: MutedWord?) {
item ?: return
lastItem = item
views.tvName.text = item.name
@ -96,13 +84,13 @@ class ActMutedWord : AppCompatActivity() {
}
private inner class MyListAdapter : BaseAdapter() {
var items: List<MyItem> = emptyList()
var items: List<MutedWord> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}
fun remove(item: MyItem?) {
fun remove(item: MutedWord?) {
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.databinding.ActNicknameBinding
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.util.backPressed
import jp.juggler.util.boolean
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.mayUri
import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.notZero
@ -134,11 +136,11 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
views.tvAcct.text = acctPretty
val ac = AcctColor.load(acctAscii, acctPretty)
colorBg = ac.color_bg
colorFg = ac.color_fg
val ac = daoAcctColor.load(acctAscii)
colorBg = ac.colorBg
colorFg = ac.colorFg
views.etNickname.setText(ac.nickname)
notificationSoundUri = ac.notification_sound
notificationSoundUri = ac.notificationSound
loadingBusy = false
show()
@ -146,14 +148,17 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
private fun save() {
if (loadingBusy) return
AcctColor(
acctAscii,
acctPretty,
views.etNickname.text.toString().trim { it <= ' ' },
colorFg,
colorBg,
notificationSoundUri
).save(System.currentTimeMillis())
launchAndShowError {
daoAcctColor.save(
System.currentTimeMillis(),
AcctColor(
nicknameSave = views.etNickname.text.toString().trim { it <= ' ' },
colorFg = colorFg,
colorBg = colorBg,
notificationSoundSaved = notificationSoundUri ?: "",
)
)
}
}
private fun show() {

View File

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

View File

@ -9,15 +9,18 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.auth.AuthRepo
import jp.juggler.subwaytooter.databinding.ActTextBinding
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.MutedWord
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.CustomShareTarget
import jp.juggler.subwaytooter.util.TootTextEncoder
import jp.juggler.subwaytooter.util.copyToClipboard
import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
@ -114,28 +117,34 @@ class ActText : AppCompatActivity() {
return super.onCreateOptionsMenu(menu)
}
private val authRepo by lazy {
AuthRepo(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
App1.setActivityTheme(this)
account = intent.long(EXTRA_ACCOUNT_DB_ID)
?.let { SavedAccount.loadAccount(this, it) }
initUI()
if (savedInstanceState == null) {
val sv = intent.string(EXTRA_TEXT) ?: ""
val contentStart = intent.int(EXTRA_CONTENT_START) ?: 0
val contentEnd = intent.int(EXTRA_CONTENT_END) ?: sv.length
views.etText.setText(sv)
launchAndShowError {
account = intent.long(EXTRA_ACCOUNT_DB_ID)
?.let { daoSavedAccount.loadAccount(it) }
// Android 9 以降ではフォーカスがないとsetSelectionできない
if (Build.VERSION.SDK_INT >= 28) {
views.etText.requestFocus()
views.etText.hideKeyboard()
if (savedInstanceState == null) {
val sv = intent.string(EXTRA_TEXT) ?: ""
val contentStart = intent.int(EXTRA_CONTENT_START) ?: 0
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() {
selection.trim().notEmpty()?.let {
try {
MutedWord.save(it)
App1.getAppState(this).onMuteUpdated()
launchAndShowError {
selection.trim().notEmpty()?.let {
daoMutedWord.save(it)
App1.getAppState(this@ActText).onMuteUpdated()
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.app.Application
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Handler
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.column.ColumnType
import jp.juggler.subwaytooter.emoji.EmojiMap
import jp.juggler.subwaytooter.global.Global
import jp.juggler.subwaytooter.global.appPref
import jp.juggler.subwaytooter.pref.LazyContextHolder
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.PrefS
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.util.*
import jp.juggler.util.data.asciiPattern
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.initializeToastUtils
import jp.juggler.util.network.MySslSocketFactory
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.os.applicationContextSafe
import jp.juggler.util.ui.*
import okhttp3.*
import okhttp3.OkHttpClient
@ -55,11 +57,17 @@ class App1 : Application() {
override fun onCreate() {
log.d("onCreate")
LazyContextHolder.init(applicationContextSafe)
super.onCreate()
initializeToastUtils(this)
prepare(applicationContext, "App1.onCreate")
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
LazyContextHolder.init(applicationContextSafe)
}
override fun onTerminate() {
log.d("onTerminate")
super.onTerminate()
@ -134,7 +142,7 @@ class App1 : Application() {
"SubwayTooter/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE}"
private fun getUserAgent(): String {
val userAgentCustom = PrefS.spUserAgent(appPref)
val userAgentCustom = PrefS.spUserAgent.value
return when {
userAgentCustom.isNotEmpty() && !reNotAllowedInUserAgent.matcher(userAgentCustom)
.find() -> userAgentCustom
@ -142,13 +150,14 @@ class App1 : Application() {
}
}
private val user_agent_interceptor = Interceptor { chain ->
chain.proceed(
chain.request().newBuilder()
.header("User-Agent", getUserAgent())
.build()
)
}
private fun userAgentInterceptor() =
Interceptor { chain ->
chain.proceed(
chain.request().newBuilder()
.header("User-Agent", getUserAgent())
.build()
)
}
private var cookieManager: CookieManager? = null
private var cookieJar: CookieJar? = null
@ -185,7 +194,7 @@ class App1 : Application() {
.connectionSpecs(Collections.singletonList(spec))
.sslSocketFactory(MySslSocketFactory, MySslSocketFactory.trustManager)
.addInterceptor(ProgressResponseBody.makeInterceptor())
.addInterceptor(user_agent_interceptor)
.addInterceptor(userAgentInterceptor())
// クッキーの導入は検討中。とりあえずmstdn.jpではクッキー有効でも改善しなかったので現時点では追加しない
// .cookieJar(cookieJar)
@ -228,8 +237,6 @@ class App1 : Application() {
initializeFont()
Global.prepare(appContext, "App1.prepare($caller)")
// 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
// the CPU with background work
@ -278,7 +285,7 @@ class App1 : Application() {
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設定はキャッシュを使わない
ok_http_client = prepareOkHttp(apiReadTimeout, apiReadTimeout)
@ -294,10 +301,11 @@ class App1 : Application() {
.build()
// 内蔵メディアビューア用のHTTP設定はタイムアウトを調整可能
val mediaReadTimeout = max(3, PrefS.spMediaReadTimeout.toInt(appPref))
ok_http_client_media_viewer = prepareOkHttp(mediaReadTimeout, mediaReadTimeout)
.cache(cache)
.build()
val mediaReadTimeout = max(3, PrefS.spMediaReadTimeout.toInt())
ok_http_client_media_viewer =
prepareOkHttp(mediaReadTimeout, mediaReadTimeout)
.cache(cache)
.build()
}
val handler = Handler(appContext.mainLooper)
@ -310,7 +318,7 @@ class App1 : Application() {
log.d("create AppState.")
state = AppState(appContext, handler, appPref)
state = AppState(appContext, handler)
appStateX = state
// getAppState()を使える状態にしてからカラム一覧をロードする
@ -421,7 +429,7 @@ class App1 : Application() {
) {
prepare(activity.applicationContext, "setActivityTheme")
var nTheme = PrefI.ipUiTheme(appPref)
var nTheme = PrefI.ipUiTheme.value
if (forceDark && nTheme == 0) nTheme = 1
activity.setTheme(
when (nTheme) {
@ -484,9 +492,8 @@ class App1 : Application() {
.url(url)
.cacheControl(CACHE_CONTROL)
.also {
val access_token = accessInfo?.getAccessToken()
if (access_token?.isNotEmpty() == true) {
it.header("Authorization", "Bearer $access_token")
accessInfo?.bearerAccessToken?.notEmpty()?.let { a ->
it.header("Authorization", "Bearer $a")
}
}
}

View File

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

View File

@ -5,10 +5,11 @@ import android.content.Context
import android.content.Intent
import jp.juggler.subwaytooter.notification.TrackingType
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.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.os.applicationContextSafe
class EventReceiver : BroadcastReceiver() {
@ -18,32 +19,36 @@ class EventReceiver : BroadcastReceiver() {
}
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,
-> {
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)
Intent.ACTION_BOOT_COMPLETED,
Intent.ACTION_MY_PACKAGE_REPLACED,
-> {
App1.prepare(context.applicationContextSafe, action)
daoNotificationTracking.resetPostAll()
}
}
}
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
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import jp.juggler.subwaytooter.notification.PollingChecker
import jp.juggler.subwaytooter.notification.restartAllWorker
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.table.NotificationCache
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.log.LogCategory
import kotlinx.coroutines.runBlocking
import java.util.*
class MyFirebaseMessagingService : FirebaseMessagingService() {
companion object {
internal val log = LogCategory("MyFirebaseMessagingService")
private val pushMessageStatus = LinkedList<String>()
// Pushメッセージが処理済みか調べる
private fun isDuplicateMessage(messageId: String) =
synchronized(pushMessageStatus) {
when (pushMessageStatus.contains(messageId)) {
true -> true
else -> {
pushMessageStatus.addFirst(messageId)
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()
restartAllWorker(this)
} catch (ex: Throwable) {
log.e(ex, "onNewToken failed")
}
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val context = this
val messageId = remoteMessage.messageId ?: return
if (isDuplicateMessage(messageId)) return
val accounts = ArrayList<SavedAccount>()
for ((key, value) in remoteMessage.data) {
log.w("onMessageReceived: $key=$value")
when (key) {
"notification_tag" -> {
SavedAccount.loadByTag(context, value).forEach { sa ->
NotificationCache.resetLastLoad(sa.db_id)
accounts.add(sa)
}
}
"acct" -> {
SavedAccount.loadAccountByAcct(context, value)?.let { sa ->
NotificationCache.resetLastLoad(sa.db_id)
accounts.add(sa)
}
}
}
}
if (accounts.isEmpty()) {
// タグにマッチする情報がなかった場合、全部読み直す
NotificationCache.resetLastLoad()
accounts.addAll(SavedAccount.loadAccountList(context))
}
log.i("accounts.size=${accounts.size} thred=${Thread.currentThread().name}")
runBlocking {
accounts.forEach {
check(it.db_id)
}
}
}
private suspend fun check(accountDbId: Long) {
try {
PollingChecker(
context = this,
accountDbId = accountDbId
).check { a, s ->
val text = "[${a.acct.pretty}]${s.desc}"
log.i(text)
}
} catch (ex: Throwable) {
log.e(ex, "check failed. accountDbId=$accountDbId")
}
}
}
//
//import com.google.firebase.messaging.FirebaseMessagingService
//import com.google.firebase.messaging.RemoteMessage
//import jp.juggler.subwaytooter.api.entity.Acct
//import jp.juggler.subwaytooter.notification.PollingChecker
//import jp.juggler.subwaytooter.notification.restartAllWorker
//import jp.juggler.subwaytooter.pref.PrefDevice
//import jp.juggler.subwaytooter.pref.prefDevice
//import jp.juggler.subwaytooter.table.SavedAccount
//import jp.juggler.subwaytooter.table.apiNotificationCache
//import jp.juggler.subwaytooter.table.apiSavedAccount
//import jp.juggler.util.log.LogCategory
//import kotlinx.coroutines.runBlocking
//import java.util.*
//
//class MyFirebaseMessagingService : FirebaseMessagingService() {
//
// companion object {
// internal val log = LogCategory("MyFirebaseMessagingService")
//
// private val pushMessageStatus = LinkedList<String>()
//
// // Pushメッセージが処理済みか調べる
// private fun isDuplicateMessage(messageId: String) =
// synchronized(pushMessageStatus) {
// when (pushMessageStatus.contains(messageId)) {
// true -> true
// else -> {
// pushMessageStatus.addFirst(messageId)
// while (pushMessageStatus.size > 100) {
// pushMessageStatus.removeLast()
// }
// false
// }
// }
// }
// }
//
// override fun onNewToken(token: String) {
// try {
// log.w("onTokenRefresh: token=$token")
// prefDevice.device
// pollingWorker2IntervalPrefDevice.from(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply()
// restartAllWorker(this)
// } catch (ex: Throwable) {
// log.e(ex, "onNewToken failed")
// }
// }
//
// override fun onMessageReceived(remoteMessage: RemoteMessage) {
// val messageId = remoteMessage.messageId ?: return
// if (isDuplicateMessage(messageId)) return
//
// val accounts = ArrayList<SavedAccount>()
// for ((key, value) in remoteMessage.data) {
// log.w("onMessageReceived: $key=$value")
// when (key) {
//// "notification_tag" -> {
//// apiSavedAccount.(context, value).forEach { sa ->
//// apiNotificationCache.resetLastLoad(sa.db_id)
//// accounts.add(sa)
//// }
//// }
// "acct" -> {
// apiSavedAccount.loadAccountByAcct(Acct.parse(value))?.let { sa ->
// apiNotificationCache.resetLastLoad(sa.db_id)
// accounts.add(sa)
// }
// }
// }
// }
//
// if (accounts.isEmpty()) {
// // タグにマッチする情報がなかった場合、全部読み直す
// apiNotificationCache.resetLastLoad()
// accounts.addAll(apiSavedAccount.loadAccountList())
// }
//
// log.i("accounts.size=${accounts.size} thred=${Thread.currentThread().name}")
// runBlocking {
// accounts.forEach {
// check(it.db_id)
// }
// }
// }
//
// private suspend fun check(accountDbId: Long) {
// try {
// PollingChecker(
// context = this,
// accountDbId = accountDbId
// ).check { a, s ->
// val text = "[${a.acct.pretty}]${s.desc}"
// log.i(text)
// }
// } 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.pref.PrefB
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.createSpan
import jp.juggler.subwaytooter.table.UserRelation
@ -43,14 +43,14 @@ fun defaultColorIcon(context: Context, iconId: Int): Drawable? =
it.setTintMode(PorterDuff.Mode.SRC_IN)
}
fun getVisibilityIconId(isMisskeyData: Boolean, visibility: TootVisibility): Int {
val isMisskey = when (PrefI.ipVisibilityStyle()) {
fun TootVisibility.getVisibilityIconId(isMisskeyData: Boolean): Int {
val isMisskey = when (PrefI.ipVisibilityStyle.value) {
PrefI.VS_MASTODON -> false
PrefI.VS_MISSKEY -> true
else -> isMisskeyData
}
return when {
isMisskey -> when (visibility) {
isMisskey -> when (this) {
TootVisibility.Public -> R.drawable.ic_public
TootVisibility.UnlistedHome -> R.drawable.ic_home
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.Mutual -> R.drawable.ic_bidirectional
}
else -> when (visibility) {
else -> when (this) {
TootVisibility.Public -> R.drawable.ic_public
TootVisibility.UnlistedHome -> R.drawable.ic_lock_open
TootVisibility.PrivateFollowers -> R.drawable.ic_lock
@ -87,19 +87,15 @@ fun getVisibilityIconId(isMisskeyData: Boolean, visibility: TootVisibility): Int
}
}
fun getVisibilityString(
context: Context,
isMisskeyData: Boolean,
visibility: TootVisibility,
): String {
val isMisskey = when (PrefI.ipVisibilityStyle()) {
fun TootVisibility.getVisibilityString(isMisskeyData: Boolean): String {
val isMisskey = when (PrefI.ipVisibilityStyle.value) {
PrefI.VS_MASTODON -> false
PrefI.VS_MISSKEY -> true
else -> isMisskeyData
}
return context.getString(
return lazyContext.getString(
when {
isMisskey -> when (visibility) {
isMisskey -> when (this) {
TootVisibility.Public -> R.string.visibility_public
TootVisibility.UnlistedHome -> R.string.visibility_home
TootVisibility.PrivateFollowers -> R.string.visibility_followers
@ -116,7 +112,7 @@ fun getVisibilityString(
TootVisibility.Limited -> R.string.visibility_limited
TootVisibility.Mutual -> R.string.visibility_mutual
}
else -> when (visibility) {
else -> when (this) {
TootVisibility.Public -> R.string.visibility_public
TootVisibility.UnlistedHome -> R.string.visibility_unlisted
TootVisibility.PrivateFollowers -> R.string.visibility_followers
@ -144,8 +140,8 @@ fun getVisibilityCaption(
visibility: TootVisibility,
): CharSequence {
val iconId = getVisibilityIconId(isMisskeyData, visibility)
val sv = getVisibilityString(context, isMisskeyData, visibility)
val iconId = visibility.getVisibilityIconId(isMisskeyData)
val sv = visibility.getVisibilityString(isMisskeyData)
val color = context.attrColor(R.attr.colorTextContent)
val sb = SpannableStringBuilder()
@ -182,11 +178,11 @@ fun setFollowIcon(
alphaMultiplier: Float,
) {
val colorFollowed =
PrefI.ipButtonFollowingColor(context.pref()).notZero()
PrefI.ipButtonFollowingColor.value.notZero()
?: context.attrColor(R.attr.colorButtonAccentFollow)
val colorFollowRequest =
PrefI.ipButtonFollowRequestColor(context.pref()).notZero()
PrefI.ipButtonFollowRequestColor.value.notZero()
?: context.attrColor(R.attr.colorButtonAccentFollowRequest)
val colorError = context.attrColor(R.attr.colorRegexFilterError)
@ -309,7 +305,7 @@ fun fixHorizontalPadding(v: View, dpDelta: Float = 12f) {
val widthDp = dm.widthPixels / dm.density
if (widthDp >= 640f && v.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
val padLr = (0.5f + dpDelta * dm.density).toInt()
when (PrefI.ipJustifyWindowContentPortrait()) {
when (PrefI.ipJustifyWindowContentPortrait.value) {
PrefI.JWCP_START -> {
v.setPaddingRelative(padLr, padT, padLr + dm.widthPixels / 2, padB)
return
@ -338,7 +334,7 @@ fun fixHorizontalMargin(v: View) {
log.d("fixHorizontalMargin: orientation=$orientationString, w=${widthDp}dp, h=${dm.heightPixels / dm.density}")
if (widthDp >= 640f && v.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
when (PrefI.ipJustifyWindowContentPortrait()) {
when (PrefI.ipJustifyWindowContentPortrait.value) {
PrefI.JWCP_START -> {
lp.marginStart = 0
lp.marginEnd = dm.widthPixels / 2
@ -397,7 +393,7 @@ fun SpannableStringBuilder.appendMisskeyReaction(
emoji == null ->
append("text")
PrefB.bpUseTwemoji(context) -> {
PrefB.bpUseTwemoji.value -> {
val start = this.length
append(text)
val end = this.length
@ -414,9 +410,10 @@ fun SpannableStringBuilder.appendMisskeyReaction(
}
fun Context.setSwitchColor(root: View?) {
root ?: return
val colorBg = attrColor(R.attr.colorWindowBackground)
val colorOff = attrColor(R.attr.colorSwitchOff)
val colorOn = PrefI.ipSwitchOnColor()
val colorOn = PrefI.ipSwitchOnColor.value
val colorDisabled = mixColor(colorBg, colorOff)
@ -451,7 +448,7 @@ fun Context.setSwitchColor(root: View?) {
)
)
root?.scan {
root.scan {
(it as? SwitchCompat)?.apply {
thumbTintList = thumbStates
trackTintList = trackStates
@ -487,13 +484,14 @@ fun AppCompatActivity.setStatusBarColor(forceDark: Boolean = false) {
var c = when {
forceDark -> Color.BLACK
else -> PrefI.ipStatusBarColor.invoke().notZero() ?: attrColor(R.attr.colorPrimaryDark)
else -> PrefI.ipStatusBarColor.value.notZero()
?: attrColor(R.attr.colorPrimaryDark)
}
setStatusBarColorCompat(c)
c = when {
forceDark -> Color.BLACK
else -> PrefI.ipNavigationBarColor()
else -> PrefI.ipNavigationBarColor.value
}
setNavigationBarColorCompat(c)
}

View File

@ -1,12 +1,13 @@
package jp.juggler.subwaytooter.action
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.Acct
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.showApiError
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.util.data.JsonObject
import jp.juggler.util.data.buildJsonObject
@ -22,7 +23,6 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
host: Host,
instanceInfoArg: TootInstance? = null,
): SavedAccount? {
try {
suspend fun AppCompatActivity.getInstanceInfo(): TootInstance? {
return try {
@ -35,7 +35,7 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
val acct = Acct.parse("?", host)
var account = SavedAccount.loadAccountByAcct(this, acct.ascii)
var account = daoSavedAccount.loadAccountByAcct(acct)
if (account != null) return account
val instanceInfo = instanceInfoArg
@ -47,7 +47,7 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
put("acct", acct.username) // ローカルから参照した場合なのでshort acct
}
val rowId = SavedAccount.insert(
val rowId = daoSavedAccount.saveNew(
acct = acct.ascii,
host = host.ascii,
domain = instanceInfo.apDomain.ascii,
@ -56,7 +56,7 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
misskeyVersion = instanceInfo.misskeyVersionMajor
)
account = SavedAccount.loadAccount(applicationContext, rowId)
account = daoSavedAccount.loadAccount(rowId)
?: error("loadAccount returns null.")
account.notification_follow = false
@ -68,7 +68,7 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
account.notification_vote = false
account.notification_post = false
account.notification_update = false
account.saveSetting()
daoSavedAccount.saveSetting(account)
return account
} catch (ex: Throwable) {
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を取得
//internal fun loadRelation1Mastodon(
// client : TootApiClient,

View File

@ -1,9 +1,7 @@
package jp.juggler.subwaytooter.action
import android.app.Dialog
import android.content.Context
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.actmain.addColumn
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.DlgCreateAccount.Companion.showUserCreateDialog
import jp.juggler.subwaytooter.dialog.LoginForm.Companion.showLoginForm
import jp.juggler.subwaytooter.notification.APP_SERVER
import jp.juggler.subwaytooter.pref.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.util.*
import jp.juggler.util.coroutine.launchIO
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.JsonObject
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.ui.dismissSafe
import kotlinx.coroutines.*
import ru.gildor.coroutines.okhttp.await
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() {
launchMain {
@ -284,105 +232,3 @@ fun ActMain.accountResendConfirmMail(accessInfo: SavedAccount) {
}.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
import android.app.AlertDialog
import android.net.Uri
import jp.juggler.subwaytooter.ActColumnList
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.handleOtherUri
import jp.juggler.subwaytooter.api.entity.TootApplication
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
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.ui.dismissSafe
@ -18,22 +19,10 @@ fun ActMain.openColumnList() =
arColumnList.launch(ActColumnList.createIntent(this, currentColumn))
// アプリをミュートする
fun ActMain.appMute(
application: TootApplication?,
confirmed: Boolean = false,
) {
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)
fun ActMain.appMute(application: TootApplication?) = launchAndShowError {
application ?: return@launchAndShowError
confirm(R.string.mute_application_confirm, application.name)
daoMutedApp.save(application.name)
appState.onMuteUpdated()
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.pickAccount
import jp.juggler.subwaytooter.getVisibilityCaption
import jp.juggler.subwaytooter.table.AcctColor
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.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
@ -212,7 +214,7 @@ private class BoostImpl(
visibility == TootVisibility.PrivateFollowers -> R.string.confirm_private_boost_from
else -> R.string.confirm_boost_from
},
AcctColor.getNickname(accessInfo)
daoAcctColor.getNickname(accessInfo)
),
when (bSet) {
true -> accessInfo.confirm_boost
@ -223,7 +225,7 @@ private class BoostImpl(
true -> accessInfo.confirm_boost = newConfirmEnabled
else -> accessInfo.confirm_unboost = newConfirmEnabled
}
accessInfo.saveSetting()
daoSavedAccount.saveSetting(accessInfo)
activity.reloadAccountSetting(accessInfo)
}
}
@ -288,7 +290,7 @@ fun ActMain.boostFromAnotherAccount(
if (isPrivateToot) {
val list = ArrayList<SavedAccount>()
for (a in SavedAccount.loadAccountList(applicationContext)) {
for (a in daoSavedAccount.loadAccountList()) {
if (a.acct == statusOwner) list.add(a)
}
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.findStatus
import jp.juggler.subwaytooter.columnviewholder.ItemListAdapter
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.dialog.actionsDialog
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.openCustomTab
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
@ -244,122 +247,126 @@ fun ActMain.conversationOtherInstance(
statusIdAccess: EntityId? = null,
isReference: Boolean = false,
) {
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(
getString(
R.string.open_web_on_host,
hostOriginal.pretty
)
) { openCustomTab(urlArg) }
// トゥートの投稿元タンスにあるアカウント
val localAccountList = ArrayList<SavedAccount>()
// トゥートの投稿元タンスにあるアカウント
val localAccountList = ArrayList<SavedAccount>()
// TLを読んだタンスにあるアカウント
val accessAccountList = ArrayList<SavedAccount>()
// TLを読んだタンスにあるアカウント
val accessAccountList = ArrayList<SavedAccount>()
// その他のタンスにあるアカウント
val otherAccountList = ArrayList<SavedAccount>()
// その他のタンスにあるアカウント
val otherAccountList = ArrayList<SavedAccount>()
for (a in daoSavedAccount.loadAccountList()) {
for (a in SavedAccount.loadAccountList(applicationContext)) {
// 疑似アカウントは後でまとめて処理する
if (a.isPseudo) continue
// 疑似アカウントは後でまとめて処理する
if (a.isPseudo) continue
if (isReference && TootInstance.getCached(a)?.canUseReference != true) 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)) {
// アクセス情報ステータスID でアクセスできるなら
// 同タンスのアカウントならステータスIDの変換なしに表示できる
localAccountList.add(a)
} 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 {
isReference -> """/references\z""".toRegex().replace(urlArg, "")
else -> urlArg
}
// 参照の場合、status URLから/references を除去しないとURLでの検索ができない
val url = when {
isReference -> """/references\z""".toRegex().replace(urlArg, "")
else -> urlArg
}
// 同タンスのアカウントがないなら、疑似アカウントで開く選択肢
if (localAccountList.isEmpty()) {
if (statusIdOriginal != null) {
dialog.addAction(
getString(R.string.open_in_pseudo_account, "?@${hostOriginal.pretty}")
) {
launchMain {
addPseudoAccount(hostOriginal)?.let { sa ->
conversationLocal(pos, sa, statusIdOriginal, isReference = isReference)
// 同タンスのアカウントがないなら、疑似アカウントで開く選択肢
if (localAccountList.isEmpty()) {
if (statusIdOriginal != null) {
action(
getString(R.string.open_in_pseudo_account, "?@${hostOriginal.pretty}")
) {
launchMain {
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}")
) {
launchMain {
addPseudoAccount(hostOriginal)?.let { sa ->
conversationRemote(pos, sa, url)
// ローカルアカウント
if (statusIdOriginal != null) {
for (a in localAccountList.sortedByNickname()) {
action(
daoAcctColor.getStringWithNickname(
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
// 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
val host = statusArg.account.apDomain
val localAccountList = ArrayList<SavedAccount>()
val otherAccountList = ArrayList<SavedAccount>()
for (a in SavedAccount.loadAccountList(this)) {
// 検索APIはログイン必須なので疑似アカウントは使えない
if (a.isPseudo) continue
if (a.matchHost(host)) {
localAccountList.add(a)
} else {
otherAccountList.add(a)
for (a in daoSavedAccount.loadAccountList()) {
when {
// 検索APIはログイン必須なので疑似アカウントは使えない
a.isPseudo -> continue
a.matchHost(host) -> localAccountList.add(a)
else -> otherAccountList.add(a)
}
}
val dialog = ActionsDialog()
val activity = this
launchAndShowError {
SavedAccount.sort(localAccountList)
for (a in localAccountList) {
dialog.addAction(
AcctColor.getStringWithNickname(
this,
R.string.open_in_account,
a.acct
)
) { step2(a) }
// step2: 選択したアカウントで投稿を検索して返信元の投稿のIDを調べる
suspend fun step2(a: SavedAccount) {
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)
}
}
}
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.runApiTask
import jp.juggler.subwaytooter.column.onFilterDeleted
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.log.showToast
@ -17,15 +17,17 @@ import okhttp3.Request
fun ActMain.openFilterMenu(accessInfo: SavedAccount, item: TootFilter?) {
item ?: return
val ad = ActionsDialog()
ad.addAction(getString(R.string.edit)) {
ActKeywordFilter.open(this, accessInfo, item.id)
val activity = this
launchAndShowError {
actionsDialog(getString(R.string.filter_of, item.displayString)) {
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(

View File

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

View File

@ -12,6 +12,7 @@ import jp.juggler.subwaytooter.api.syncAccountByAcct
import jp.juggler.subwaytooter.column.onListMemberUpdated
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
@ -71,7 +72,8 @@ fun ActMain.listMemberAdd(
"".toFormRequestBody().toPost()
) ?: return@runApiTask null
val relation = accessInfo.saveUserRelation(
val relation = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject)
) ?: 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.dialog.pickAccount
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.pref.prefDevice
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.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
@ -37,7 +40,7 @@ fun ActPost.saveWindowSize() {
// WindowMetrics#getBounds() the window size including all system bar areas
windowManager?.currentWindowMetrics?.bounds?.let { 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 {
@Suppress("DEPRECATION")
@ -45,7 +48,7 @@ fun ActPost.saveWindowSize() {
val dm = DisplayMetrics()
display.getMetrics(dm)
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,
) {
val useManyWindow = PrefB.bpManyWindowPost(pref)
val useMultiWindow = useManyWindow || PrefB.bpMultiWindowPost(pref)
val useManyWindow = PrefB.bpManyWindowPost.value
val useMultiWindow = useManyWindow || PrefB.bpMultiWindowPost.value
val intent = ActPost.createIntent(
context = this,
@ -99,7 +102,9 @@ fun ActMain.openActPostImpl(
ActPost.refActPost?.get()
?.takeIf { it.isLiveActivity }
?.let {
it.updateText(intent)
launchAndShowError {
it.updateText(intent)
}
return
}
}
@ -108,11 +113,10 @@ fun ActMain.openActPostImpl(
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
var options = ActivityOptionsCompat.makeBasic()
PrefDevice.loadPostWindowBound(this)
?.let {
log.d("ActPost launchBounds $it")
options = options.setLaunchBounds(it)
}
prefDevice.loadPostWindowBound()?.let {
log.d("ActPost launchBounds $it")
options = options.setLaunchBounds(it)
}
arActPost.launch(intent, options)
}
@ -254,7 +258,7 @@ fun ActMain.quoteFromAnotherAccount(
fun ActMain.quoteName(who: TootAccount) {
var sv = who.display_name
try {
val fmt = PrefS.spQuoteNameFormat(pref)
val fmt = PrefS.spQuoteNameFormat.value
if (fmt.contains("%1\$s")) {
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.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.table.AcctColor
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.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
@ -122,11 +124,15 @@ fun ActMain.reactionAdd(
)
val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg)
confirm(
getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(accessInfo)),
getString(
R.string.confirm_reaction,
emojiSpan,
daoAcctColor.getNickname(accessInfo)
),
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(
R.string.confirm_reaction_to_pleroma,
emojiSpan,
AcctColor.getNickname(accessInfo),
daoAcctColor.getNickname(accessInfo),
resolvedStatus.account.acct.host?.pretty ?: "(null)"
)
else -> confirm(
getString(R.string.confirm_reaction, emojiSpan, AcctColor.getNickname(accessInfo)),
getString(
R.string.confirm_reaction,
emojiSpan,
daoAcctColor.getNickname(accessInfo)
),
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.TootStatus
import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor
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.util.coroutine.launchAndShowError
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) {
ActionsDialog()
.addAction(getString(R.string.edit)) {
scheduledPostEdit(accessInfo, item)
}
.addAction(getString(R.string.delete)) {
launchAndShowError {
scheduledPostDelete(accessInfo, item)
column.onScheduleDeleted(item)
showToast(false, R.string.scheduled_post_deleted)
launchAndShowError {
actionsDialog {
action(getString(R.string.edit)) {
scheduledPostEdit(accessInfo, item)
}
action(getString(R.string.delete)) {
launchAndShowError {
scheduledPostDelete(accessInfo, item)
column.onScheduleDeleted(item)
showToast(false, R.string.scheduled_post_deleted)
}
}
}
.show(this)
}
}
fun ActMain.launchActText(intent: Intent) = arActText.launch(intent)
@ -125,7 +129,7 @@ fun ActMain.favourite(
true -> R.string.confirm_favourite_from
else -> R.string.confirm_unfavourite_from
},
AcctColor.getNickname(accessInfo)
daoAcctColor.getNickname(accessInfo)
),
when (bSet) {
true -> accessInfo.confirm_favourite
@ -136,7 +140,7 @@ fun ActMain.favourite(
true -> accessInfo.confirm_favourite = newConfirmEnabled
else -> accessInfo.confirm_unfavourite = newConfirmEnabled
}
accessInfo.saveSetting()
daoSavedAccount.saveSetting(accessInfo)
reloadAccountSetting(accessInfo)
}
}
@ -294,12 +298,12 @@ fun ActMain.bookmark(
confirm(
getString(
R.string.confirm_unbookmark_from,
AcctColor.getNickname(accessInfo)
daoAcctColor.getNickname(accessInfo)
),
accessInfo.confirm_unbookmark
) { newConfirmEnabled ->
accessInfo.confirm_unbookmark = newConfirmEnabled
accessInfo.saveSetting()
daoSavedAccount.saveSetting(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.column.ColumnType
import jp.juggler.subwaytooter.column.onTagFollowChanged
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.dialog.actionsDialog
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.openCustomTab
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.encodePercent
import jp.juggler.util.log.LogCategory
@ -57,23 +60,21 @@ fun ActMain.tagDialog(
) {
val activity = this
val tagWithSharp = "#$tagWithoutSharp"
launchMain {
try {
val d = ActionsDialog()
.addAction(getString(R.string.open_hashtag_column)) {
tagTimelineFromAccount(
pos,
url,
host,
tagWithoutSharp
)
}
launchAndShowError {
actionsDialog(tagWithSharp) {
action(getString(R.string.open_hashtag_column)) {
tagTimelineFromAccount(
pos,
url,
host,
tagWithoutSharp
)
}
// 投稿者別タグTL
if (whoAcct != null) {
d.addAction(
AcctColor.getStringWithNickname(
action(
daoAcctColor.getStringWithNickname(
activity,
R.string.open_hashtag_from_account,
whoAcct
@ -89,13 +90,13 @@ fun ActMain.tagDialog(
}
}
d.addAction(getString(R.string.open_in_browser)) { openCustomTab(url) }
.addAction(
getString(
R.string.quote_hashtag_of,
tagWithSharp
)
) { openPost("$tagWithSharp ") }
action(getString(R.string.open_in_browser)) {
openCustomTab(url)
}
action(getString(R.string.quote_hashtag_of, tagWithSharp)) {
openPost("$tagWithSharp ")
}
if (tagList != null && tagList.size > 1) {
val sb = StringBuilder()
@ -104,11 +105,8 @@ fun ActMain.tagDialog(
sb.append(s)
}
val tagAll = sb.toString()
d.addAction(
getString(
R.string.quote_all_hashtag_of,
tagAll
)
action(
getString(R.string.quote_all_hashtag_of, tagAll)
) { openPost("$tagAll ") }
}
@ -118,10 +116,12 @@ fun ActMain.tagDialog(
if (tag == null) {
val result = runApiTask(accessInfo) { client ->
client.request("/api/v1/tags/${tagWithoutSharp.encodePercent()}")
} ?: return@launchMain //cancelled.
TootParser(activity, accessInfo)
.tag(result.jsonObject)
?.let { tag = it }
}
if (result != null) {
TootParser(activity, accessInfo)
.tag(result.jsonObject)
?.let { tag = it }
}
}
val toggle = !(tag?.following ?: false)
@ -129,14 +129,10 @@ fun ActMain.tagDialog(
true -> R.string.follow_hashtag_of
else -> R.string.unfollow_hashtag_of
}
d.addAction(getString(toggleCaption, tagWithSharp)) {
action(getString(toggleCaption, tagWithSharp)) {
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を指定する
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
// 分類する
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)
// ミスキーはアカウント別タグTLがないので
// アカウント別タグTLを開けない
a.isMisskey -> Unit
!a.matchHost(host) -> listOther.add(a)
else -> listOriginal.add(a)
}
}
}
} else {
when {
// 疑似アカウントはacctからaccount idを取得できないので
// アカウント別タグTLを開けない
a.isPseudo -> Unit
// ミスキーはアカウント別タグTLがないので
// アカウント別タグTLを開けない
a.isMisskey -> Unit
// ブラウザで表示する
if (!url.isNullOrBlank()) {
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(

View File

@ -12,6 +12,9 @@ import jp.juggler.subwaytooter.api.syncStatus
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.dialog.pickAccount
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.util.coroutine.launchMain
import jp.juggler.util.log.showToast
@ -102,7 +105,7 @@ fun ActMain.timelineLocal(
launchMain {
// 指定タンスのアカウントを持ってるか?
val accountList = ArrayList<SavedAccount>()
for (a in SavedAccount.loadAccountList(applicationContext)) {
for (a in daoSavedAccount.loadAccountList()) {
if (a.matchHost(host)) accountList.add(a)
}
@ -113,12 +116,11 @@ fun ActMain.timelineLocal(
}
} else {
// 持ってるならアカウントを選んで開く
SavedAccount.sort(accountList)
pickAccount(
bAllowPseudo = true,
bAuto = false,
message = getString(R.string.account_picker_add_timeline_of, host),
accountListArg = accountList
accountListArg = accountList.sortedByNickname()
)?.let { addColumn(pos, it, ColumnType.LOCAL) }
}
}
@ -168,7 +170,7 @@ fun ActMain.timelineAroundByStatusAnotherAccount(
// 利用可能なアカウントを列挙する
val accountList1 = ArrayList<SavedAccount>() // 閲覧アカウントとホストが同じ
val accountList2 = ArrayList<SavedAccount>() // その他実アカウント
label@ for (a in SavedAccount.loadAccountList(this)) {
label@ for (a in daoSavedAccount.loadAccountList()) {
// Misskeyアカウントはステータスの同期が出来ないので選択させない
if (a.isNA || a.isMisskey) continue
when {
@ -179,8 +181,8 @@ fun ActMain.timelineAroundByStatusAnotherAccount(
!a.isPseudo -> accountList2.add(a)
}
}
SavedAccount.sort(accountList1)
SavedAccount.sort(accountList2)
accountList1.sortInplaceByNickname()
accountList2.sortInplaceByNickname()
accountList1.addAll(accountList2)
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.ReportForm
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.FavMute
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.subwaytooter.util.openCustomTab
import jp.juggler.util.*
@ -72,9 +69,9 @@ fun ActMain.openAvatarImage(who: TootAccount) {
fun ActMain.clickHideFavourite(
accessInfo: SavedAccount,
who: TootAccount,
) {
) = launchAndShowError {
val acct = accessInfo.getFullAcct(who)
FavMute.save(acct)
daoFavMute.save(acct)
showToast(false, R.string.changed)
for (column in appState.columnList) {
column.onHideFavouriteNotification(acct)
@ -84,8 +81,8 @@ fun ActMain.clickHideFavourite(
fun ActMain.clickShowFavourite(
accessInfo: SavedAccount,
who: TootAccount,
) {
FavMute.delete(accessInfo.getFullAcct(who))
) = launchAndShowError {
daoFavMute.delete(accessInfo.getFullAcct(who))
showToast(false, R.string.changed)
}
@ -110,133 +107,134 @@ private fun ActMain.userMute(
bMute: Boolean,
bMuteNotification: Boolean,
duration: Int?,
) {
) = launchAndShowError {
val whoAcct = whoAccessInfo.getFullAcct(whoArg)
if (accessInfo.isMe(whoAcct)) {
showToast(false, R.string.it_is_you)
return
return@launchAndShowError
}
launchMain {
var resultRelation: UserRelation? = null
var resultWhoId: EntityId? = null
runApiTask(accessInfo) { client ->
val parser = TootParser(this, accessInfo)
if (accessInfo.isPseudo) {
if (!whoAcct.isValidFull) {
TootApiResult("can't mute pseudo acct ${whoAcct.pretty}")
} else {
val relation = UserRelation.loadPseudo(whoAcct)
relation.muting = bMute
relation.savePseudo(whoAcct.ascii)
resultRelation = relation
resultWhoId = whoArg.id
TootApiResult()
var resultRelation: UserRelation? = null
var resultWhoId: EntityId? = null
runApiTask(accessInfo) { client ->
val parser = TootParser(this, accessInfo)
if (accessInfo.isPseudo) {
if (!whoAcct.isValidFull) {
TootApiResult("can't mute pseudo acct ${whoAcct.pretty}")
} else {
val relation = daoUserRelation.loadPseudo(whoAcct)
relation.muting = bMute
daoUserRelation.savePseudo(whoAcct.ascii, relation)
resultRelation = relation
resultWhoId = whoArg.id
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 {
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 = 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)
)
}
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 = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, jsonObject)
)
}
}
}
}?.let { result ->
val relation = resultRelation
val whoId = resultWhoId
if (relation == null || whoId == null) {
showToast(false, result.error)
} else {
// 未確認だが、自分をミュートしようとするとリクエストは成功するがレスポンス中のmutingはfalseになるはず
if (bMute && !relation.muting) {
showToast(false, R.string.not_muted)
return@launchMain
}
}
}?.let { result ->
val relation = resultRelation
val whoId = resultWhoId
if (relation == null || whoId == null) {
showToast(false, result.error)
} else {
// 未確認だが、自分をミュートしようとするとリクエストは成功するがレスポンス中のmutingはfalseになるはず
if (bMute && !relation.muting) {
showToast(false, R.string.not_muted)
return@launchAndShowError
}
for (column in appState.columnList) {
if (column.accessInfo.isPseudo) {
if (relation.muting && column.type != ColumnType.PROFILE) {
// ミュートしたユーザの情報はTLから消える
column.removeAccountInTimelinePseudo(whoAcct)
}
// フォローアイコンの表示更新が走る
column.updateFollowIcons(accessInfo)
} else if (column.accessInfo == accessInfo) {
when {
!relation.muting -> {
if (column.type == ColumnType.MUTES) {
// ミュート解除したら「ミュートしたユーザ」カラムから消える
column.removeUser(accessInfo, ColumnType.MUTES, whoId)
} else {
// 他のカラムではフォローアイコンの表示更新が走る
column.updateFollowIcons(accessInfo)
}
}
column.type == ColumnType.PROFILE && column.profileId == whoId -> {
// 該当ユーザのプロフページのトゥートはミュートしてても見れる
// しかしフォローアイコンの表示更新は必要
for (column in appState.columnList) {
if (column.accessInfo.isPseudo) {
if (relation.muting && column.type != ColumnType.PROFILE) {
// ミュートしたユーザの情報はTLから消える
column.removeAccountInTimelinePseudo(whoAcct)
}
// フォローアイコンの表示更新が走る
column.updateFollowIcons(accessInfo)
} else if (column.accessInfo == accessInfo) {
when {
!relation.muting -> {
if (column.type == ColumnType.MUTES) {
// ミュート解除したら「ミュートしたユーザ」カラムから消える
column.removeUser(accessInfo, ColumnType.MUTES, whoId)
} else {
// 他のカラムではフォローアイコンの表示更新が走る
column.updateFollowIcons(accessInfo)
}
}
else -> {
// ミュートしたユーザの情報はTLから消える
column.removeAccountInTimeline(accessInfo, whoId)
}
column.type == ColumnType.PROFILE && column.profileId == 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,
who: TootAccount,
whoAccessInfo: SavedAccount,
) {
) = launchAndShowError {
val activity = this@userMuteConfirm
// Mastodon 3.3から時限ミュート設定ができる
@ -288,59 +286,56 @@ fun ActMain.userMuteConfirm(
cbMuteNotification.vg(hasMuteNotification)
?.setText(R.string.confirm_mute_notification_for_user)
launchMain {
val spMuteDuration: Spinner = view.findViewById(R.id.spMuteDuration)
val hasMuteDuration = try {
when {
accessInfo.isMisskey || accessInfo.isPseudo -> false
else -> {
var resultBoolean = false
runApiTask(accessInfo) { client ->
val (ti, ri) = TootInstance.get(client)
resultBoolean = ti?.versionGE(TootInstance.VERSION_3_3_0_rc1) == true
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)
val spMuteDuration: Spinner = view.findViewById(R.id.spMuteDuration)
val hasMuteDuration = try {
when {
accessInfo.isMisskey || accessInfo.isPseudo -> false
else -> {
var resultBoolean = false
runApiTask(accessInfo) { client ->
val (ti, ri) = TootInstance.get(client)
resultBoolean = ti?.versionGE(TootInstance.VERSION_3_3_0_rc1) == true
ri
}
resultBoolean
}
}
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()
} catch (ignored: CancellationException) {
// not show error
return@launchAndShowError
} catch (ex: RuntimeException) {
showToast(true, ex.message)
return@launchAndShowError
}
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(
@ -381,9 +376,9 @@ fun ActMain.userBlock(
if (whoAcct.ascii.contains('?')) {
TootApiResult("can't block pseudo account ${whoAcct.pretty}")
} else {
val relation = UserRelation.loadPseudo(whoAcct)
val relation = daoUserRelation.loadPseudo(whoAcct)
relation.blocking = bBlock
relation.savePseudo(whoAcct.ascii)
daoUserRelation.savePseudo(whoAcct.ascii, relation)
relationResult = relation
TootApiResult()
}
@ -397,11 +392,10 @@ fun ActMain.userBlock(
whoIdResult = whoId
if (accessInfo.isMisskey) {
fun saveBlock(v: Boolean) {
val ur = UserRelation.load(accessInfo.db_id, whoId)
val ur = daoUserRelation.load(accessInfo.db_id, whoId)
ur.blocking = v
UserRelation.save1Misskey(
daoUserRelation.save1Misskey(
System.currentTimeMillis(),
accessInfo.db_id,
whoId.toString(),
@ -434,7 +428,8 @@ fun ActMain.userBlock(
"".toFormRequestBody().toPost()
)?.also { result ->
val parser = TootParser(this, accessInfo)
relationResult = accessInfo.saveUserRelation(
relationResult = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject)
)
}
@ -597,7 +592,7 @@ fun ActMain.userProfileFromAnotherAccount(
bAuto = false,
message = getString(
R.string.account_picker_open_user_who,
AcctColor.getNickname(accessInfo, who)
daoAcctColor.getNickname(accessInfo, who)
),
accountListArg = accountListNonPseudo(who.apDomain)
)?.let { ai ->
@ -630,7 +625,7 @@ fun ActMain.userProfile(
acct: Acct,
userUrl: String,
originalUrl: String = userUrl,
) {
) = launchAndShowError {
if (accessInfo?.isPseudo == false) {
// 文脈のアカウントがあり、疑似アカウントではない
@ -655,51 +650,49 @@ fun ActMain.userProfile(
}
}
}
return
return@launchAndShowError
}
// 文脈がない、もしくは疑似アカウントだった
// 疑似アカウントでは検索APIを使えないため、IDが分からない
if (!SavedAccount.hasRealAccount()) {
if (!daoSavedAccount.hasRealAccount()) {
// 疑似アカウントしか登録されていない
// chrome tab で開く
openCustomTab(originalUrl)
return
return@launchAndShowError
}
launchMain {
val activity = this@userProfile
pickAccount(
bAllowPseudo = false,
bAuto = false,
message = getString(
R.string.account_picker_open_user_who,
AcctColor.getNickname(acct)
),
accountListArg = accountListNonPseudo(acct.host),
extraCallback = { ll, pad_se, pad_tb ->
// chrome tab で開くアクションを追加
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
val b = AppCompatButton(activity)
b.setPaddingRelative(pad_se, pad_tb, pad_se, pad_tb)
b.gravity = Gravity.START or Gravity.CENTER_VERTICAL
b.isAllCaps = false
b.layoutParams = lp
b.minHeight = (0.5f + 32f * activity.density).toInt()
b.text = getString(R.string.open_in_browser)
b.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
val activity = this@userProfile
pickAccount(
bAllowPseudo = false,
bAuto = false,
message = getString(
R.string.account_picker_open_user_who,
daoAcctColor.getNickname(acct)
),
accountListArg = accountListNonPseudo(acct.host),
extraCallback = { ll, pad_se, pad_tb ->
// chrome tab で開くアクションを追加
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
val b = AppCompatButton(activity)
b.setPaddingRelative(pad_se, pad_tb, pad_se, pad_tb)
b.gravity = Gravity.START or Gravity.CENTER_VERTICAL
b.isAllCaps = false
b.layoutParams = lp
b.minHeight = (0.5f + 32f * activity.density).toInt()
b.text = getString(R.string.open_in_browser)
b.setBackgroundResource(R.drawable.btn_bg_transparent_round6dp)
b.setOnClickListener {
openCustomTab(originalUrl)
}
ll.addView(b, 0)
b.setOnClickListener {
openCustomTab(originalUrl)
}
)?.let {
userProfileFromUrlOrAcct(pos, it, acct, userUrl)
ll.addView(b, 0)
}
)?.let {
userProfileFromUrlOrAcct(pos, it, acct, userUrl)
}
}
@ -798,12 +791,9 @@ fun ActMain.userSetShowBoosts(
jsonObjectOf("reblogs" to bShow).toPostRequestBuilder()
)?.also { result ->
val parser = TootParser(this, accessInfo)
resultRelation = accessInfo.saveUserRelation(
parseItem(
::TootRelationShip,
parser,
result.jsonObject
)
resultRelation = daoUserRelation.saveUserRelation(
accessInfo,
parseItem(::TootRelationShip, parser, result.jsonObject)
)
}
}?.let { result ->
@ -860,7 +850,7 @@ fun ActMain.userSetStatusNotification(
result.jsonObject
)
if (relation != null) {
UserRelation.save1Mastodon(
daoUserRelation.save1Mastodon(
System.currentTimeMillis(),
accessInfo.db_id,
relation
@ -901,7 +891,8 @@ fun ActMain.userEndorsement(
)
?.also { result ->
val parser = TootParser(this, accessInfo)
resultRelation = accessInfo.saveUserRelation(
resultRelation = daoUserRelation.saveUserRelation(
accessInfo,
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.pref.PrefL
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoSavedAccount
// デフォルトの投稿先アカウントを探す。アカウント選択が必要な状況ならnull
val ActMain.currentPostTarget: SavedAccount?
@ -17,9 +18,9 @@ val ActMain.currentPostTarget: SavedAccount?
},
{ env ->
val dbId = PrefL.lpTabletTootDefaultAccount()
val dbId = PrefL.lpTabletTootDefaultAccount.value
if (dbId != -1L) {
val a = SavedAccount.loadAccount(this@currentPostTarget, dbId)
val a = daoSavedAccount.loadAccount(dbId)
if (a != null && !a.isPseudo) return a
}
@ -47,24 +48,23 @@ val ActMain.currentPostTarget: SavedAccount?
})
fun ActMain.reloadAccountSetting(
newAccounts: ArrayList<SavedAccount> = SavedAccount.loadAccountList(
this
),
newAccounts: List<SavedAccount>,
) {
for (column in appState.columnList) {
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()
}
}
fun ActMain.reloadAccountSetting(account: SavedAccount) {
val newData = SavedAccount.loadAccount(this, account.db_id)
val newData = daoSavedAccount.loadAccount(account.db_id)
?: return
for (column in appState.columnList) {
val a = column.accessInfo
if (a.acct != newData.acct) continue
if (!a.isNA) a.reloadSetting(this, newData)
if (!a.isNA) daoSavedAccount.reloadSetting(a, newData)
column.fireShowColumnHeader()
}
}

View File

@ -5,6 +5,7 @@ import android.text.Spannable
import android.view.View
import android.widget.TextView
import androidx.core.view.GravityCompat
import androidx.work.WorkManager
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R
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.ViewHolderHeaderBase
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.pref.*
import jp.juggler.subwaytooter.push.PushWorker
import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.util.checkPrivacyPolicy
import jp.juggler.subwaytooter.util.openCustomTab
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.addTo
import jp.juggler.util.data.cast
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.ui.dismissSafe
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.TimeUnit
private val log = LogCategory("ActMainActions")
fun ActMain.onBackPressedImpl() {
launchAndShowError {
// メニューが開いていたら閉じる
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START)
return
}
// カラムが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
)
}
// メニューが開いていたら閉じる
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START)
return@launchAndShowError
}
else /* PrefI.BACK_ASK_ALWAYS */ -> {
val closeableColumnList = getClosableColumnList()
val dialog = ActionsDialog()
if (closeableColumnList.size == 1) {
val column = closeableColumnList.first()
dialog.addAction(getString(R.string.close_column)) {
closeColumn(column, bConfirmed = true)
// カラムが0個ならアプリを終了する
if (appState.columnCount == 0) {
finish()
return@launchAndShowError
}
// カラム設定が開いているならカラム設定を閉じる
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() }
dialog.addAction(getString(R.string.app_exit)) { finish() }
dialog.show(this, null)
/* PrefI.BACK_ASK_ALWAYS */
else -> actionsDialog {
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 ipUiTheme = PrefI.ipUiTheme
val now = System.currentTimeMillis()
// テーマが未定義でなければ警告しない
if (pref.getInt(ipUiTheme.key, -1) != -1) {
if (lazyPref.getInt(ipUiTheme.key, -1) != -1) {
log.i("themeDefaultChangedDialog: theme was set.")
return
}
// 頻繁には警告しない
if (now - lpThemeDefaultChangedWarnTime.invoke(pref) < TimeUnit.DAYS.toMillis(60L)) {
if (now - lpThemeDefaultChangedWarnTime.value < TimeUnit.DAYS.toMillis(60L)) {
log.i("themeDefaultChangedDialog: avoid frequently check.")
return
}
pref.edit().put(lpThemeDefaultChangedWarnTime, now).apply()
lpThemeDefaultChangedWarnTime.value = now
// 色がすべてデフォルトなら警告不要
val customizedKeys = ArrayList<String>()
@ -202,18 +210,50 @@ fun ActMain.themeDefaultChangedDialog() {
item.pref?.let { p ->
when {
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()) {
pref.edit().put(ipUiTheme, ipUiTheme.defVal).apply()
ipUiTheme.value = ipUiTheme.defVal
return
}
AlertDialog.Builder(this)
.setMessage(R.string.color_theme_changed)
.setPositiveButton(android.R.string.ok, null)
.show()
log.w("themeDefaultChangedDialog: customizedKeys=${customizedKeys.joinToString(",")}")
suspendCancellableCoroutine { cont ->
val dialog = AlertDialog.Builder(this)
.setMessage(R.string.color_theme_changed)
.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
}
val refreshAfterToot = PrefI.ipRefreshAfterToot(pref)
val refreshAfterToot = PrefI.ipRefreshAfterToot.value
if (refreshAfterToot != PrefI.RAT_DONT_REFRESH) {
appState.columnList
.filter { it.accessInfo.acct == postedAcct }

View File

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

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import jp.juggler.subwaytooter.ActMain
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.runApiTask2
import jp.juggler.subwaytooter.api.showApiError
import jp.juggler.subwaytooter.auth.authRepo
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.startLoading
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.dialog.runInProgress
import jp.juggler.subwaytooter.notification.PushSubscriptionHelper
import jp.juggler.subwaytooter.notification.checkNotificationImmediate
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
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.daoSavedAccount
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.decodePercent
import jp.juggler.util.data.groupEx
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
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")
@ -154,21 +170,22 @@ fun ActMain.handleOtherUri(uri: Uri): Boolean {
return false
}
private fun ActMain.handleCustomSchemaUri(uri: Uri) {
private fun ActMain.handleCustomSchemaUri(uri: Uri) = launchAndShowError {
val dataIdString = uri.getQueryParameter("db_id")
if (dataIdString != null) {
// subwaytooter://notification_click/?db_id=(db_id)
handleNotificationClick(uri, dataIdString)
} else {
if (dataIdString == null) {
// OAuth2 認証コールバック
// subwaytooter://oauth(\d*)/?...
handleOAuth2Callback(uri)
} else {
// subwaytooter://notification_click/?db_id=(db_id)
handleNotificationClick(uri, dataIdString)
}
}
private fun ActMain.handleNotificationClick(uri: Uri, dataIdString: String) {
try {
val account = dataIdString.toLongOrNull()?.let { SavedAccount.loadAccount(this, it) }
val account = dataIdString.toLongOrNull()
?.let { daoSavedAccount.loadAccount(it) }
if (account == null) {
showToast(true, "handleNotificationClick: missing SavedAccount. id=$dataIdString")
return
@ -227,7 +244,7 @@ fun ActMain.afterAccountVerify(auth2Result: Auth2Result): Boolean = auth2Result.
// 「アカウント追加のハズが既存アカウントで認証していた」
// 「アクセストークン更新のハズが別アカウントで認証していた」
// などを防止するため、full acctでアプリ内DBを検索
when (val sa = SavedAccount.loadAccountByAcct(this@afterAccountVerify, newAcct.ascii)) {
when (val sa = daoSavedAccount.loadAccountByAcct(newAcct)) {
null -> afterAccountAdd(newAcct, auth2Result)
else -> afterAccessTokenUpdate(auth2Result, sa)
}
@ -238,10 +255,10 @@ private fun ActMain.afterAccessTokenUpdate(
sa: SavedAccount,
): Boolean {
// DBの情報を更新する
sa.updateTokenInfo(auth2Result)
authRepo.updateTokenInfo(sa, auth2Result)
// 各カラムの持つアカウント情報をリロードする
reloadAccountSetting()
reloadAccountSetting(daoSavedAccount.loadAccountList())
// 自動でリロードする
appState.columnList
@ -252,6 +269,7 @@ private fun ActMain.afterAccessTokenUpdate(
PushSubscriptionHelper.clearLastCheck(sa)
checkNotificationImmediateAll(this, onlySubscription = true)
checkNotificationImmediate(this, sa.db_id)
updatePushDistributer()
showToast(false, R.string.access_token_updated_for, sa.acct.pretty)
return true
@ -263,7 +281,7 @@ private fun ActMain.afterAccountAdd(
): Boolean {
val ta = auth2Result.tootAccount
val rowId = SavedAccount.insert(
val rowId = daoSavedAccount.saveNew(
acct = newAcct.ascii,
host = auth2Result.apiHost.ascii,
domain = auth2Result.apDomain.ascii,
@ -271,7 +289,7 @@ private fun ActMain.afterAccountAdd(
token = auth2Result.tokenJson,
misskeyVersion = auth2Result.tootInstance.misskeyVersionMajor,
)
val account = SavedAccount.loadAccount(applicationContext, rowId)
val account = daoSavedAccount.loadAccount(rowId)
if (account == null) {
showToast(false, "loadAccount failed.")
return false
@ -298,13 +316,13 @@ private fun ActMain.afterAccountAdd(
}
if (bModified) {
account.saveSetting()
daoSavedAccount.saveSetting(account)
}
}
// 適当にカラムを追加する
addColumn(false, defaultInsertPosition, account, ColumnType.HOME)
if (SavedAccount.count == 1) {
if (daoSavedAccount.isSingleAccount()) {
addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS)
addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL)
addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE)
@ -313,6 +331,7 @@ private fun ActMain.afterAccountAdd(
// 通知の更新が必要かもしれない
checkNotificationImmediateAll(this, onlySubscription = true)
checkNotificationImmediate(this, account.db_id)
updatePushDistributer()
showToast(false, R.string.account_confirmed)
return true
}
@ -329,3 +348,79 @@ fun ActMain.handleSharedIntent(intent: 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() {
etQuickPost.typeface = ActMain.timelineFont
if (!PrefB.bpQuickPostBar.invoke(pref)) {
if (!PrefB.bpQuickPostBar.value) {
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.imeOptions = EditorInfo.IME_ACTION_NONE
// 最後に指定する必要がある?
@ -70,7 +70,7 @@ fun ActMain.initUIQuickPost() {
fun ActMain.showQuickPostVisibility() {
btnQuickPostMenu.imageResource =
when (val resId = getVisibilityIconId(false, quickPostVisibility)) {
when (val resId = quickPostVisibility.getVisibilityIconId(false)) {
R.drawable.ic_question -> R.drawable.ic_description
else -> resId
}
@ -82,7 +82,7 @@ fun ActMain.toggleQuickPostMenu() {
fun ActMain.performQuickPost(account: SavedAccount?) {
if (account == null) {
val a = if (tabletViews != null && !PrefB.bpQuickTootOmitAccountSelection(pref)) {
val a = if (tabletViews != null && !PrefB.bpQuickTootOmitAccountSelection.value) {
// タブレットモードでオプションが無効なら
// 簡易投稿は常にアカウント選択する
null

View File

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

View File

@ -3,18 +3,23 @@ package jp.juggler.subwaytooter.actmain
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.view.MyViewPager
// スマホモードならラムダを実行する。タブレットモードなら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を返す
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) }
tabletViews?.let { return codeTablet(it) }
error("missing phoneViews/tabletViews")
@ -27,7 +32,7 @@ fun ActMain.initPhoneTablet() {
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
phoneViews = ActMainPhoneViews(this).apply {
initUI(tmpPhonePager)

View File

@ -92,7 +92,7 @@ class ActMainTabletViews(val actMain: ActMain) {
// if( animator is DefaultItemAnimator){
// animator.supportsChangeAnimations = false
// }
if (PrefB.bpTabletSnap()) {
if (PrefB.bpTabletSnap.value) {
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.PrefS
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.accountListCanSeeMyReactions
import jp.juggler.subwaytooter.util.VersionString
import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchIO
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.decodeJsonObject
import jp.juggler.util.data.decodeUTF8
@ -120,7 +121,7 @@ class SideMenuAdapter(
)
)
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)
},
Item(icon = R.drawable.ic_face, title = R.string.reactioned_posts) {
launchMain {
launchAndShowError {
accountListCanSeeMyReactions()?.let { list ->
if (list.isEmpty()) {
showToast(false, R.string.not_available_for_current_accounts)
@ -518,7 +519,7 @@ class SideMenuAdapter(
private fun getTimeZoneString(context: Context): String {
try {
var tz = TimeZone.getDefault()
val tzId = PrefS.spTimeZone()
val tzId = PrefS.spTimeZone.value
if (tzId.isBlank()) {
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.api.entity.TootVisibility
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.AcctColor
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.data.notZero
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 }))
val ac = AcctColor.load(a)
val ac = daoAcctColor.load(a)
views.btnAccount.text = ac.nickname
if (AcctColor.hasColorBackground(ac)) {
if (daoAcctColor.hasColorBackground(ac)) {
views.btnAccount.background =
getAdaptiveRippleDrawableRound(this, ac.color_bg, ac.color_fg)
getAdaptiveRippleDrawableRound(this, ac.colorBg, ac.colorFg)
} else {
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)
}
updateTextCount()
@ -75,8 +77,7 @@ fun ActPost.performAccountChooser() {
if (!canSwitchAccount()) return
if (isMultiWindowPost) {
accountList = SavedAccount.loadAccountList(this)
SavedAccount.sort(accountList)
accountList = daoSavedAccount.loadAccountList().sortedByNickname()
}
launchMain {

View File

@ -12,13 +12,14 @@ import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.calcIconRound
import jp.juggler.subwaytooter.defaultColorIcon
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgFocusPoint
import jp.juggler.subwaytooter.dialog.DlgTextInput
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.util.AttachmentRequest
import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
@ -169,7 +170,7 @@ fun ActPost.onPostAttachmentCompleteImpl(pa: PostAttachment) {
log.i("onPostAttachmentComplete: upload complete.")
// 投稿欄の末尾に追記する
if (PrefB.bpAppendAttachmentUrlToContent.invoke(pref)) {
if (PrefB.bpAppendAttachmentUrlToContent.value) {
appendArrachmentUrl(a)
}
}
@ -215,35 +216,33 @@ fun ActPost.performAttachmentClick(idx: Int) {
showToast(false, ex.withCaption("can't get attachment item[$idx]."))
return
}
val a = ActionsDialog()
.addAction(getString(R.string.set_description)) {
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)
launchAndShowError {
actionsDialog(getString(R.string.media_attachment)) {
action(getString(R.string.set_description)) {
editAttachmentDescription(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) {

View File

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

View File

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

View File

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

View File

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

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

View File

@ -34,7 +34,7 @@ abstract class AuthBase {
val clientName
get() = arrayOf(
testClientName,
PrefS.spClientName.invoke(),
PrefS.spClientName.value,
).firstNotNullOfOrNull { it.notBlank() }
?: 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.InstanceType
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.table.ClientInfo
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoClientInfo
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonObject
@ -84,7 +84,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
}
}
clientInfo[KEY_CLIENT_CREDENTIAL] = clientCredential
ClientInfo.save(apiHost, clientName, clientInfo.toString())
daoClientInfo.save(apiHost, clientName, clientInfo.toString())
return clientCredential
}
@ -95,7 +95,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
tootInstance: TootInstance?,
forceUpdateClient: Boolean,
): JsonObject {
var clientInfo = ClientInfo.load(apiHost, clientName)
var clientInfo = daoClientInfo.load(apiHost, clientName)
// スコープ一覧を取得する
val scopeString = mastodonScope(tootInstance)
@ -126,7 +126,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
else -> try {
// マストドン2.4でスコープが追加された
// 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない
ClientInfo.delete(apiHost, clientName)
daoClientInfo.delete(apiHost, clientName)
// クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
// client credential だけは消せる
@ -254,7 +254,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
when {
param.startsWith("db:") -> try {
val dataId = param.substring(3).toLong(10)
val sa = SavedAccount.loadAccount(context, dataId)
val sa = daoSavedAccount.loadAccount(dataId)
?: error("missing account db_id=$dataId")
client.account = sa
} catch (ex: Throwable) {
@ -272,7 +272,7 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
val apiHost = client.apiHost
?: 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")
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.Host
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.table.ClientInfo
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.table.daoClientInfo
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.*
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"}
*/
private suspend fun createAuthUri(apiHost: Host, appSecret: String): Uri {
PrefDevice.from(context).edit().apply {
putString(PrefDevice.LAST_AUTH_INSTANCE, apiHost.ascii)
putString(PrefDevice.LAST_AUTH_SECRET, appSecret)
when (val account = account) {
null -> remove(PrefDevice.LAST_AUTH_DB_ID)
else -> putLong(PrefDevice.LAST_AUTH_DB_ID, account.db_id)
}
}.apply()
context.prefDevice.saveLastAuth(
host = apiHost.ascii,
secret = appSecret,
dbId = account?.db_id, //nullable
)
return api.authSessionGenerate(apiHost, appSecret)
.string("url").notEmpty()?.toUri()
?: error("missing 'url' in session/generate.")
@ -122,7 +120,7 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
): Uri {
val apiHost = apiHost ?: error("missing apiHost")
val clientInfo = ClientInfo.load(apiHost, clientName)
val clientInfo = daoClientInfo.load(apiHost, clientName)
// スコープ一覧を取得する
val scopeArray = getScopeArrayMisskey(ti)
@ -173,7 +171,7 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
val appSecret = appJson.string(KEY_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)
}
@ -183,20 +181,21 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
*/
override suspend fun authStep2(uri: Uri): Auth2Result {
val prefDevice = PrefDevice.from(context)
val prefDevice = context.prefDevice
val token = uri.getQueryParameter("token")
?.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.")
val apiHost = Host.parse(hostStr)
when (val dbId = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID, -1L)) {
when (val dbId = prefDevice.lastAuthDbId) {
// new registration
-1L -> client.apiHost = apiHost
null -> client.apiHost = apiHost
// update access token
else -> SavedAccount.loadAccount(context, dbId)?.also {
else -> daoSavedAccount.loadAccount(dbId)?.also {
client.account = it
} ?: error("missing account db_id=$dbId")
}
@ -209,7 +208,7 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
)
@Suppress("UNUSED_VARIABLE")
val clientInfo = ClientInfo.load(apiHost, clientName)
val clientInfo = daoClientInfo.load(apiHost, clientName)
?.notEmpty() ?: error("missing client id")
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.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty
@ -60,14 +60,11 @@ class MisskeyAuth13(override val client: TootApiClient) : AuthBase() {
val sessionId = UUID.randomUUID().toString()
PrefDevice.from(client.context).edit().apply {
putString(PrefDevice.LAST_AUTH_INSTANCE, apiHost.ascii)
putString(PrefDevice.LAST_AUTH_SECRET, sessionId)
when (val account = account) {
null -> remove(PrefDevice.LAST_AUTH_DB_ID)
else -> putLong(PrefDevice.LAST_AUTH_DB_ID, account.db_id)
}
}.apply()
client.context.prefDevice.saveLastAuth(
host = apiHost.ascii,
secret = sessionId,
dbId = account?.db_id,
)
return api13.createAuthUrl(
apiHost = apiHost,
@ -82,20 +79,20 @@ class MisskeyAuth13(override val client: TootApiClient) : AuthBase() {
override suspend fun authStep2(uri: Uri): Auth2Result {
// 認証開始時に保存した情報
val prefDevice = PrefDevice.from(client.context)
val savedSessionId = prefDevice.getString(PrefDevice.LAST_AUTH_SECRET, null)
val prefDevice = client.context.prefDevice
val savedSessionId = prefDevice.lastAuthSecret
val apiHost = prefDevice.getString(PrefDevice.LAST_AUTH_INSTANCE, null)
val apiHost = prefDevice.lastAuthInstance
?.let { Host.parse(it) }
?: error("missing apiHost")
when (val dbId = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID, -1L)) {
when (val dbId = prefDevice.lastAuthDbId) {
// new registration
-1L -> client.apiHost = apiHost
null -> client.apiHost = apiHost
// update access token
else -> {
val sa = SavedAccount.loadAccount(context, dbId)
val sa = daoSavedAccount.loadAccount(dbId)
?: error("missing account db_id=$dbId")
client.account = sa
}
@ -131,7 +128,7 @@ class MisskeyAuth13(override val client: TootApiClient) : AuthBase() {
.account(accountJson)
?: error("can't parse user json.")
prefDevice.edit().remove(PrefDevice.LAST_AUTH_SECRET).apply()
prefDevice.removeLastAuth()
return Auth2Result(
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 String.decode(): EntityId? {
fun String.decodeEntityId(): EntityId? {
if (this.isEmpty()) return null
// first character is 'L' for EntityIdLong, 'S' for EntityIdString.
// 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) =
intent?.string(key)?.decode()
intent?.string(key)?.decodeEntityId()
fun from(bundle: Bundle?, key: String) =
bundle?.string(key)?.decode()
bundle?.string(key)?.decodeEntityId()
// 内部保存データのデコード用。APIレスポンスのパースに使ってはいけない
fun from(data: JsonObject?, key: String): EntityId? {
val o = data?.get(key)
if (o is Long) return EntityId(o.toString())
return (o as? String)?.decode()
return (o as? String)?.decodeEntityId()
}
fun from(cursor: Cursor, key: String) =
cursor.getStringOrNull(key)?.decode()
cursor.getStringOrNull(key)?.decodeEntityId()
}
private fun encode(): String {
val prefix = 'S'
return "$prefix$this"
}
// 昔は文字列とLong値を区別していたが、今はもうない
fun encode(): String = "S$this"
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.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
@ -215,7 +216,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
this.fields = parseMisskeyFields(src)
UserRelation.fromAccount(parser, src, id)
daoUserRelation.fromAccount(parser, src, id)
@Suppress("LeakingThis")
MisskeyAccountDetailMap.fromAccount(parser, this, id)
@ -504,7 +505,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
.append(suggestionSource)
}
if (PrefB.bpDirectoryLastActive() && last_status_at > 0L) {
if (PrefB.bpDirectoryLastActive.value && last_status_at > 0L) {
prepareSb()
.append(context.getString(R.string.last_active))
.append(delm)
@ -519,7 +520,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
}
if (!fromProfileHeader) {
if (PrefB.bpDirectoryTootCount() &&
if (PrefB.bpDirectoryTootCount.value &&
(statuses_count ?: 0L) > 0L
) {
prepareSb()
@ -528,8 +529,8 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
.append(statuses_count.toString())
}
if (PrefB.bpDirectoryFollowers() &&
!PrefB.bpHideFollowCount() &&
if (PrefB.bpDirectoryFollowers.value &&
!PrefB.bpHideFollowCount.value &&
(followers_count ?: 0L) > 0L
) {
prepareSb()
@ -538,7 +539,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
.append(followers_count.toString())
}
if (PrefB.bpDirectoryNote() && note?.isNotEmpty() == true) {
if (PrefB.bpDirectoryNote.value && note?.isNotEmpty() == true) {
val decodedNote = DecodeOptions(
context,
accessInfo,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import android.content.Context
import android.text.Spannable
import android.text.SpannableString
import androidx.annotation.StringRes
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootAccountMap
import jp.juggler.subwaytooter.api.TootParser
@ -365,7 +364,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
this.mentions = mergeMentions(mentions1, mentions2)
this.decoded_mentions =
HTMLDecoder.decodeMentions(parser.linkHelper, this)
HTMLDecoder.decodeMentions(parser, this)
?: EMPTY_SPANNABLE
// contentを読んだ後にアンケートのデコード
@ -484,7 +483,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
this.muted = false
this.language = null
this.decoded_mentions =
HTMLDecoder.decodeMentions(parser.linkHelper, this)
HTMLDecoder.decodeMentions(parser, this)
?: EMPTY_SPANNABLE
val quote = when {
@ -696,7 +695,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
this.muted = src.optBoolean("muted")
this.language = src.string("language")?.notEmpty()
this.decoded_mentions =
HTMLDecoder.decodeMentions(parser.linkHelper, this)
HTMLDecoder.decodeMentions(parser, this)
?: EMPTY_SPANNABLE
val quote = when {
@ -1100,7 +1099,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
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) {
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")
@Volatile
internal var muted_app: HashSet<String>? = null
internal var muted_app: Set<String>? = null
@Volatile
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)
}
if (bAllowRelative && PrefB.bpRelativeTimestamp()) {
if (bAllowRelative && PrefB.bpRelativeTimestamp.value) {
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.SharedPreferences
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.provider.BaseColumns
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.ColumnEncoder
import jp.juggler.subwaytooter.column.getBackgroundImageDir
import jp.juggler.subwaytooter.global.appDatabase
import jp.juggler.subwaytooter.pref.PrefL
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.util.*
import jp.juggler.util.data.*
@ -117,50 +115,49 @@ object AppDataExporter {
writer.name(jsonKey)
writer.beginArray()
appDatabase.query(table, null, null, null, null, null, null)
?.use { cursor ->
val names = ArrayList<String>()
val column_count = cursor.columnCount
for (i in 0 until column_count) {
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()
}
appDatabase.rawQuery("select from $table", emptyArray()).use { cursor ->
val names = ArrayList<String>()
val column_count = cursor.columnCount
for (i in 0 until column_count) {
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()
}
}
writer.endArray()
}
@ -198,19 +195,21 @@ object AppDataExporter {
}
if (SavedAccount.table == table) {
// 一時的に存在したが現在のDBスキーマにはない項目は読み飛ばす
if ("nickname" == name || "color" == name) {
reader.skipValue()
continue
}
// リアルタイム通知に関連する項目は読み飛ばす
if (SavedAccount.COL_NOTIFICATION_TAG.name == name ||
SavedAccount.COL_REGISTER_KEY.name == name ||
SavedAccount.COL_REGISTER_TIME.name == name
) {
reader.skipValue()
continue
when (name) {
// 一時的に存在したが現在のDBスキーマにはない項目は読み飛ばす
"nickname",
"color",
"notification_server",
"register_key",
"register_time",
"last_notification_error",
"last_subscription_error",
"last_push_endpoint",
-> {
reader.skipValue()
continue
}
else -> Unit
}
}
@ -230,8 +229,7 @@ object AppDataExporter {
}
}
reader.endObject()
val new_id =
db.insertWithOnConflict(table, null, cv, SQLiteDatabase.CONFLICT_REPLACE)
val new_id = db.replace(table, null, cv)
if (new_id == -1L) error("importTable: invalid row_id")
idMap?.put(old_id, new_id)
}
@ -359,7 +357,7 @@ object AppDataExporter {
val app_state = App1.getAppState(context)
writePref(writer, app_state.pref)
writePref(writer, lazyPref)
writeFromTable(writer, KEY_ACCOUNT, SavedAccount.table)
writeFromTable(writer, KEY_ACCT_COLOR, AcctColor.table)
@ -386,12 +384,12 @@ object AppDataExporter {
while (reader.hasNext()) {
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_ACCT_COLOR -> {
importTable(reader, AcctColor.table, null)
AcctColor.clearMemoryCache()
daoAcctColor.clearMemoryCache()
}
KEY_MUTED_APP -> importTable(reader, MutedApp.table, null)
@ -408,10 +406,10 @@ object AppDataExporter {
}
run {
val old_id = PrefL.lpTabletTootDefaultAccount(app_state.pref)
val old_id = PrefL.lpTabletTootDefaultAccount.value
if (old_id != -1L) {
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.appcompat.widget.AppCompatImageView
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.itemviewholder.AdditionalButtonsPosition
import jp.juggler.subwaytooter.pref.*
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.openBrowser
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast
import jp.juggler.util.data.intentOpenDocument
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.getAdaptiveRippleDrawable
import jp.juggler.util.ui.getAdaptiveRippleDrawableRound
import kotlinx.coroutines.delay
import org.jetbrains.anko.backgroundDrawable
import java.util.concurrent.atomic.AtomicInteger
@ -227,6 +233,10 @@ class AppSettingItem(
val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_setting).apply {
section(R.string.notifications) {
action(R.string.push_distributor) {
action = { selectPushDistributor() }
desc = R.string.push_distributor_desc
}
text(
PrefS.spPullNotificationCheckInterval,
@ -447,14 +457,18 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
) {
val lp = pref.cast<LongPref>()!!
spinnerInitializer = { spinner ->
val adapter = AccountAdapter()
spinner.adapter = adapter
spinner.setSelection(adapter.getIndexFromId(lp(pref)))
launchAndShowError {
val list = daoSavedAccount.loadAccountList()
.sortedByNickname()
val adapter = AccountAdapter(list)
spinner.adapter = adapter
spinner.setSelection(adapter.getIndexFromId(lp.value))
}
}
spinnerOnSelected = { spinner, index ->
val adapter = spinner.adapter.cast<ActAppSetting.AccountAdapter>()
?: error("spinnerOnSelected: missing AccountAdapter")
pref.edit().put(lp, adapter.getIdFromIndex(index)).apply()
spinner.adapter.cast<ActAppSetting.AccountAdapter>()
?.getIdFromIndex(index)
?.let { lp.value = it }
}
}
@ -522,12 +536,12 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
spinnerInitializer = { spinner ->
val adapter = TimeZoneAdapter()
spinner.adapter = adapter
spinner.setSelection(adapter.getIndexFromId(sp(pref)))
spinner.setSelection(adapter.getIndexFromId(sp.value))
}
spinnerOnSelected = { spinner, index ->
val adapter = spinner.adapter.cast<ActAppSetting.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 = {
pref.edit().remove(item.pref?.key).apply()
item.pref?.removeValue()
showTimelineFont(item)
}
showTextView = { showTimelineFont(item, it) }
@ -602,7 +616,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
}
}
onClickReset = {
pref.edit().remove(item.pref?.key).apply()
item.pref?.removeValue()
showTimelineFont(AppSettingItem.TIMELINE_FONT_BOLD)
}
showTextView = { showTimelineFont(item, it) }
@ -621,7 +635,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
fromFloat = { formatFontSize(it) }
captionFontSize = {
val fv = fp(pref)
val fv = fp.value
when {
!fv.isFinite() -> PrefF.default_timeline_font_size
fv < 1f -> 1f
@ -629,7 +643,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
}
}
captionSpacing = {
PrefS.spTimelineSpacing(pref).toFloatOrNull()
PrefS.spTimelineSpacing.value.toFloatOrNull()
}
changed = {
findItemViewHolder(item)?.updateCaption()
@ -644,7 +658,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
fromFloat = { formatFontSize(it) }
captionFontSize = {
val fv = fp(pref)
val fv = fp.value
when {
!fv.isFinite() -> PrefF.default_acct_font_size
fv < 1f -> 1f
@ -667,7 +681,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
fromFloat = { formatFontSize(it) }
captionFontSize = {
val fv = fp(pref)
val fv = fp.value
when {
!fv.isFinite() -> PrefF.default_notification_tl_font_size
fv < 1f -> 1f
@ -675,7 +689,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
}
}
captionSpacing = {
PrefS.spTimelineSpacing(pref).toFloatOrNull()
PrefS.spTimelineSpacing.value.toFloatOrNull()
}
changed = {
findItemViewHolder(item)?.updateCaption()
@ -718,7 +732,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
fromFloat = { formatFontSize(it) }
captionFontSize = {
val fv = fp(pref)
val fv = fp.value
when {
!fv.isFinite() -> PrefF.default_header_font_size
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 tvColumnName: TextView = viewRoot.findViewById(R.id.tvColumnName)
val colorColumnHeaderBg = PrefI.ipCcdHeaderBg(activity.pref)
val colorColumnHeaderFg = PrefI.ipCcdHeaderFg(activity.pref)
val colorColumnHeaderBg = PrefI.ipCcdHeaderBg.value
val colorColumnHeaderFg = PrefI.ipCcdHeaderFg.value
val headerBg = when {
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 tvSampleContent: TextView = viewRoot.findViewById(R.id.tvSampleContent)
val colorColumnBg = PrefI.ipCcdContentBg(activity.pref)
val colorColumnAcct = PrefI.ipCcdContentAcct(activity.pref)
val colorColumnText = PrefI.ipCcdContentText(activity.pref)
val colorColumnBg = PrefI.ipCcdContentBg.value
val colorColumnAcct = PrefI.ipCcdContentAcct.value
val colorColumnText = PrefI.ipCcdContentText.value
flColumnBackground.setBackgroundColor(colorColumnBg) // may 0
@ -909,7 +923,6 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
group(R.string.footer_color) {
AppSettingItem.SAMPLE_FOOTER =
sample(R.layout.setting_sample_footer) { activity, viewRoot ->
val pref = activity.pref
val ivFooterToot: AppCompatImageView = viewRoot.findViewById(R.id.ivFooterToot)
val ivFooterMenu: AppCompatImageView = viewRoot.findViewById(R.id.ivFooterMenu)
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 vIndicator: View = viewRoot.findViewById(R.id.vIndicator)
val footerButtonBgColor = PrefI.ipFooterButtonBgColor(pref)
val footerButtonFgColor = PrefI.ipFooterButtonFgColor(pref)
val footerTabBgColor = PrefI.ipFooterTabBgColor(pref)
val footerTabDividerColor = PrefI.ipFooterTabDividerColor(pref)
val footerTabIndicatorColor = PrefI.ipFooterTabIndicatorColor(pref)
val footerButtonBgColor = PrefI.ipFooterButtonBgColor.value
val footerButtonFgColor = PrefI.ipFooterButtonFgColor.value
val footerTabBgColor = PrefI.ipFooterTabBgColor.value
val footerTabDividerColor = PrefI.ipFooterTabDividerColor.value
val footerTabIndicatorColor = PrefI.ipFooterTabIndicatorColor.value
val colorColumnStripBackground = footerTabBgColor.notZero()
?: 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) {

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

View File

@ -192,7 +192,7 @@ fun Column.removeNotifications() {
listData.clear()
duplicateMap.clear()
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
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.JsonObject
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_STR] = ac.nickname
dst[KEY_COLUMN_ACCESS_COLOR] = ac.color_fg
dst[KEY_COLUMN_ACCESS_COLOR_BG] = ac.color_bg
dst[KEY_COLUMN_ACCESS_COLOR] = ac.colorFg
dst[KEY_COLUMN_ACCESS_COLOR_BG] = ac.colorBg
dst[KEY_COLUMN_NAME] = getColumnName(true)
dst[KEY_OLD_INDEX] = oldIndex
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.auth.authRepo
import jp.juggler.subwaytooter.columnviewholder.scrollToTop
import jp.juggler.subwaytooter.notification.injectData
import jp.juggler.subwaytooter.pref.PrefB
@ -33,7 +34,7 @@ class ColumnTask_Loading(
override suspend fun background(): TootApiResult? {
ctStarted.set(true)
if (PrefB.bpOpenSticker(pref)) {
if (PrefB.bpOpenSticker.value) {
OpenSticker.loadAndWait()
}
@ -52,7 +53,7 @@ class ColumnTask_Loading(
client.account = accessInfo
try {
accessInfo.checkConfirmed(context, client)
context.authRepo.checkConfirmed(accessInfo, 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.data.JsonArray
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPostRequestBuilder
@ -157,8 +158,8 @@ class ColumnTask_Refresh(
App1.sound(it)
}
}
o.highlightSpeech?.let {
column.appState.addSpeech(it.name, dedupMode = DedupMode.RecentExpire)
o.highlightSpeech?.name.notEmpty()?.let {
column.appState.addSpeech(it, dedupMode = DedupMode.RecentExpire)
}
}
}
@ -306,7 +307,7 @@ class ColumnTask_Refresh(
isCancelled -> false
listTmp?.isNotEmpty() != true -> false
willAddGap -> true
else -> PrefB.bpForceGap()
else -> PrefB.bpForceGap.value
}
if (doesAddGap()) {
@ -495,7 +496,7 @@ class ColumnTask_Refresh(
if (!isCancelled &&
listTmp?.isNotEmpty() == true &&
(willAddGap || PrefB.bpForceGap(context))
(willAddGap || PrefB.bpForceGap.value)
) {
addOne(listTmp, TootGap.mayNull(maxId, lastSinceId), head = addToHead)
}
@ -582,7 +583,7 @@ class ColumnTask_Refresh(
if (!isCancelled &&
listTmp?.isNotEmpty() == true &&
(willAddGap || PrefB.bpForceGap(context))
(willAddGap || PrefB.bpForceGap.value)
) {
addOne(listTmp, TootGap.mayNull(maxId, lastSinceId), head = addToHead)
}
@ -690,16 +691,14 @@ class ColumnTask_Refresh(
params.apply {
if (!bBottom) {
if (first) {
addRangeMisskey(bBottom)
addRangeMisskey(bBottom = false)
} else {
putMisskeySince(column.idRecent)
}
} else {
if (first) {
when (column.pagingType) {
ColumnPagingType.Default -> addRangeMisskey(bBottom)
ColumnPagingType.Default -> addRangeMisskey(bBottom = true)
ColumnPagingType.Offset -> put("offset", column.offsetNext)
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.refreshTootsearch
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.data.*
import jp.juggler.util.log.LogCategory
@ -685,7 +685,7 @@ enum class ColumnType(
R.string.profile_of,
when (who) {
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.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.AcctSet
import jp.juggler.subwaytooter.table.TagSet
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.table.daoAcctSet
import jp.juggler.subwaytooter.table.daoTagHistory
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.util.data.toJsonArray
import jp.juggler.util.log.LogCategory
import jp.juggler.util.network.toPostRequestBuilder
import kotlin.math.min
class UserRelationLoader(val column: Column) {
companion object {
@ -63,7 +64,13 @@ class UserRelationLoader(val column: Column) {
while (start < end) {
var step = end - start
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
}
log.d("updateRelation: update $end relations.")
@ -108,7 +115,11 @@ class UserRelationLoader(val column: Column) {
for (i in 0 until list.size) {
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.")
@ -134,7 +145,7 @@ class UserRelationLoader(val column: Column) {
}
val result = client.request(sb.toString()) ?: break // cancelled.
val list = parseList(::TootRelationShip, parser, result.jsonArray)
if (list.size > 0) UserRelation.saveListMastodon(
if (list.size > 0) daoUserRelation.saveListMastodon(
now,
column.accessInfo.db_id,
list
@ -156,7 +167,7 @@ class UserRelationLoader(val column: Column) {
while (n < acctList.size) {
var length = size - n
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
}
log.d("updateRelation: update $n acct.")
@ -171,11 +182,10 @@ class UserRelationLoader(val column: Column) {
val now = System.currentTimeMillis()
n = 0
while (n < tagList.size) {
var length = size - n
if (length > Column.ACCT_DB_STEP) length = Column.ACCT_DB_STEP
TagSet.saveList(now, tagList, n, length)
n += length
while (n < size) {
val step = min(Column.ACCT_DB_STEP, size - n)
daoTagHistory.saveList(now, tagList, n, step)
n += step
}
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.pref.PrefB
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.view.MyLinkMovementMethod
import jp.juggler.subwaytooter.view.MyTextView
@ -219,15 +219,15 @@ class ColumnViewHolder(
val column = this.column
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.setTextColor(
ac.color_fg.notZero()
ac.colorFg.notZero()
?: activity.attrColor(R.attr.colorTimeSmall)
)
tvColumnContext.setBackgroundColor(ac.color_bg)
tvColumnContext.setBackgroundColor(ac.colorBg)
tvColumnContext.setPaddingRelative(activity.acctPadLr, 0, activity.acctPadLr, 0)
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.itemAnimator = null
@ -432,7 +432,7 @@ class ColumnViewHolder(
cbWithHighlight,
).forEach { it.setOnCheckedChangeListener(this) }
if (PrefB.bpMoveNotificationsQuickFilter(activity.pref)) {
if (PrefB.bpMoveNotificationsQuickFilter.value) {
(svQuickFilter.parent as? ViewGroup)?.removeView(svQuickFilter)
llColumnSettingInside.addView(svQuickFilter, 0)

View File

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

View File

@ -48,7 +48,7 @@ fun ColumnViewHolder.closeBitmaps() {
fun ColumnViewHolder.loadBackgroundImage(iv: ImageView, url: String?) {
try {
if (url == null || url.isEmpty() || PrefB.bpDontShowColumnBackgroundImage(activity.pref)) {
if (url == null || url.isEmpty() || PrefB.bpDontShowColumnBackgroundImage.value) {
// 指定がないなら閉じる
closeBitmaps()
return
@ -128,7 +128,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, pageIdx: Int, pageCount: Int)
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)
tvColumnStatus.text = "?"
@ -221,7 +221,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, pageIdx: Int, pageCount: Int)
btnEmojiAdd.vg(false)
etSearch.vg(true)
btnSearchClear.vg(PrefB.bpShowSearchClear(activity.pref))
btnSearchClear.vg(PrefB.bpShowSearchClear.value)
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()
val context = activity
val announcementsBgColor = PrefI.ipAnnouncementsBgColor().notZero()
val announcementsBgColor = PrefI.ipAnnouncementsBgColor.value.notZero()
?: context.attrColor(R.attr.colorSearchFormBackground)
btnAnnouncementsCutout.apply {
@ -294,7 +294,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, pageIdx: Int, pageCount: Int)
setPadding(0, padV, 0, padV)
}
val searchBgColor = PrefI.ipSearchBgColor().notZero()
val searchBgColor = PrefI.ipSearchBgColor.value.notZero()
?: context.attrColor(R.attr.colorSearchFormBackground)
llSearch.apply {

View File

@ -30,7 +30,7 @@ fun ColumnViewHolder.showQuickFilter() {
btnQuickFilterReaction.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

View File

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

View File

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

View File

@ -1,35 +1,50 @@
package jp.juggler.subwaytooter.dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.R
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 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 addAction(caption: CharSequence, action: () -> Unit): ActionsDialog {
actionList.add(Action(caption, action))
return this
fun action(caption: CharSequence, action: suspend () -> Unit) {
list.add(Action(caption, action))
}
fun show(context: Context, title: CharSequence? = null): ActionsDialog {
AlertDialog.Builder(context).apply {
setNegativeButton(R.string.cancel, null)
setItems(actionList.map { it.caption }.toTypedArray()) { _, which ->
if (which >= 0 && which < actionList.size) {
actionList[which].action()
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun showSuspend(context: Context): Action =
suspendCancellableCoroutine { cont ->
val dialog = android.app.AlertDialog.Builder(context).apply {
title?.notEmpty()?.let { setTitle(it) }
setNegativeButton(android.R.string.cancel, null)
setItems(list.map { it.caption }.toTypedArray()) { d, i ->
if (cont.isActive) cont.resume(list[i]) {}
d.dismissSafe()
}
}
title?.notEmpty()?.let { setTitle(it) }
}.show()
return this
}
setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException())
}
}.create()
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")
suspend fun AppCompatActivity.confirm(
suspend inline fun AppCompatActivity.confirm(
message: String,
getConfirmEnabled: Boolean,
isConfirmEnabled: Boolean,
setConfirmEnabled: (newConfirmEnabled: Boolean) -> Unit,
) {
if (!getConfirmEnabled) return
suspendCancellableCoroutine<Unit> { cont ->
if (!isConfirmEnabled) return
val skipNext = suspendCancellableCoroutine { cont ->
try {
val views = DlgConfirmBinding.inflate(layoutInflater)
views.tvMessage.text = message
@ -79,10 +79,7 @@ object DlgConfirm {
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
if (views.cbSkipNext.isChecked) {
setConfirmEnabled(false)
}
if (cont.isActive) cont.resume(Unit)
if (cont.isActive) cont.resume(views.cbSkipNext.isChecked)
}
dialog.setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException("dialog cancelled."))
@ -92,6 +89,7 @@ object DlgConfirm {
cont.resumeWithException(ex)
}
}
if (skipNext) setConfirmEnabled(false)
}
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