add ktlint plugin to project and apply default code style (#2209)

* add ktlint plugin to project and apply default code style

* some manual adjustments, fix wildcard imports

* update CONTRIBUTING.md

* fix formatting
This commit is contained in:
Konrad Pozniak 2021-06-28 21:13:24 +02:00 committed by GitHub
commit 16ffcca748
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
227 changed files with 3933 additions and 3371 deletions

View file

@ -4,5 +4,5 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
class BindingHolder<T : ViewBinding>(
val binding: T
val binding: T
) : RecyclerView.ViewHolder(binding.root)

View file

@ -74,18 +74,20 @@ object BlurHashDecoder {
val g = (value / 19) % 19
val b = value % 19
return floatArrayOf(
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
signedPow2((b - 9) / 9.0f) * maxAc
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
signedPow2((b - 9) / 9.0f) * maxAc
)
}
private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
private fun composeBitmap(
width: Int, height: Int,
numCompX: Int, numCompY: Int,
colors: Array<FloatArray>
width: Int,
height: Int,
numCompX: Int,
numCompY: Int,
colors: Array<FloatArray>
): Bitmap {
val imageArray = IntArray(width * height)
for (y in 0 until height) {
@ -118,13 +120,12 @@ object BlurHashDecoder {
}
private val charMap = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
)
.mapIndexed { i, c -> c to i }
.toMap()
.mapIndexed { i, c -> c to i }
.toMap()
}

View file

@ -4,4 +4,4 @@ enum class CardViewMode {
NONE,
FULL_WIDTH,
INDENTED
}
}

View file

@ -22,10 +22,10 @@ import android.widget.MultiAutoCompleteTextView
class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer {
private fun isMentionOrHashtagAllowedCharacter(character: Char) : Boolean {
return Character.isLetterOrDigit(character) || character == '_' // simple usernames
|| character == '-' // extended usernames
|| character == '.' // domain dot
private fun isMentionOrHashtagAllowedCharacter(character: Char): Boolean {
return Character.isLetterOrDigit(character) || character == '_' || // simple usernames
character == '-' || // extended usernames
character == '.' // domain dot
}
override fun findTokenStart(text: CharSequence, cursor: Int): Int {
@ -36,8 +36,8 @@ class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer {
var character = text[i - 1]
// go up to first illegal character or character we're looking for (@, # or :)
while(i > 0 && !(character == '@' || character == '#' || character == ':')) {
if(!isMentionOrHashtagAllowedCharacter(character)) {
while (i > 0 && !(character == '@' || character == '#' || character == ':')) {
if (!isMentionOrHashtagAllowedCharacter(character)) {
return cursor
}
@ -46,13 +46,13 @@ class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer {
}
// maybe caught domain name? try search username
if(i > 2 && character == '@') {
if (i > 2 && character == '@') {
var j = i - 1
var character2 = text[i - 2]
// again go up to first illegal character or tag "@"
while(j > 0 && character2 != '@') {
if(!isMentionOrHashtagAllowedCharacter(character2)) {
while (j > 0 && character2 != '@') {
if (!isMentionOrHashtagAllowedCharacter(character2)) {
break
}
@ -61,15 +61,16 @@ class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer {
}
// found mention symbol, override cursor
if(character2 == '@') {
if (character2 == '@') {
i = j
character = character2
}
}
if (i < 1
|| (character != '@' && character != '#' && character != ':')
|| i > 1 && !Character.isWhitespace(text[i - 2])) {
if (i < 1 ||
(character != '@' && character != '#' && character != ':') ||
i > 1 && !Character.isWhitespace(text[i - 2])
) {
return cursor
}
return i - 1

View file

@ -18,17 +18,16 @@ package com.keylesspalace.tusky.util
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.*
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.text.SpannableStringBuilder
import android.text.style.ReplacementSpan
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
import com.keylesspalace.tusky.entity.Emoji
import java.lang.ref.WeakReference
import java.util.regex.Pattern
@ -39,8 +38,8 @@ import java.util.regex.Pattern
* @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable)
* @return the text with the shortcodes replaced by EmojiSpans
*/
fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean) : CharSequence {
if(emojis.isNullOrEmpty())
fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean): CharSequence {
if (emojis.isNullOrEmpty())
return this
val builder = SpannableStringBuilder.valueOf(this)
@ -49,7 +48,7 @@ fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean) : C
val matcher = Pattern.compile(":$shortcode:", Pattern.LITERAL)
.matcher(this)
while(matcher.find()) {
while (matcher.find()) {
val span = EmojiSpan(WeakReference(view))
builder.setSpan(span, matcher.start(), matcher.end(), 0)
@ -64,8 +63,8 @@ fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean) : C
class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSpan() {
var imageDrawable: Drawable? = null
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?) : Int {
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
if (fm != null) {
/* update FontMetricsInt or otherwise span does not get drawn when
* it covers the whole text */
@ -75,10 +74,10 @@ class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSpan()
fm.descent = metrics.descent
fm.bottom = metrics.bottom
}
return (paint.textSize * 1.2).toInt()
}
override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
imageDrawable?.let { drawable ->
canvas.save()
@ -94,15 +93,15 @@ class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSpan()
canvas.restore()
}
}
fun getTarget(animate : Boolean): Target<Drawable> {
fun getTarget(animate: Boolean): Target<Drawable> {
return object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
viewWeakReference.get()?.let { view ->
if(animate && resource is Animatable) {
if (animate && resource is Animatable) {
val callback = resource.callback
resource.callback = object: Drawable.Callback {
resource.callback = object : Drawable.Callback {
override fun unscheduleDrawable(p0: Drawable, p1: Runnable) {
callback?.unscheduleDrawable(p0, p1)
}
@ -121,7 +120,7 @@ class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSpan()
view.invalidate()
}
}
override fun onLoadCleared(placeholder: Drawable?) {}
}
}

View file

@ -20,9 +20,9 @@ import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
abstract class CustomFragmentStateAdapter(
private val activity: FragmentActivity
): FragmentStateAdapter(activity) {
private val activity: FragmentActivity
) : FragmentStateAdapter(activity) {
fun getFragment(position: Int): Fragment?
= activity.supportFragmentManager.findFragmentByTag("f" + getItemId(position))
fun getFragment(position: Int): Fragment? =
activity.supportFragmentManager.findFragmentByTag("f" + getItemId(position))
}

View file

@ -44,4 +44,4 @@ sealed class Either<out L, out R> {
Right(mapper(this.asRight()))
}
}
}
}

View file

@ -29,13 +29,14 @@ import kotlin.math.max
* This class bundles information about an emoji font as well as many convenient actions.
*/
class EmojiCompatFont(
val name: String,
private val display: String,
@StringRes val caption: Int,
@DrawableRes val img: Int,
val url: String,
// The version is stored as a String in the x.xx.xx format (to be able to compare versions)
val version: String) {
val name: String,
private val display: String,
@StringRes val caption: Int,
@DrawableRes val img: Int,
val url: String,
// The version is stored as a String in the x.xx.xx format (to be able to compare versions)
val version: String
) {
private val versionCode = getVersionCode(version)
@ -102,8 +103,13 @@ class EmojiCompatFont(
if (compareVersions(fileExists.second, versionCode) < 0) {
val file = fileExists.first
// Uses side effects!
Log.d(TAG, String.format("Deleted %s successfully: %s", file.absolutePath,
file.delete()))
Log.d(
TAG,
String.format(
"Deleted %s successfully: %s", file.absolutePath,
file.delete()
)
)
}
}
}
@ -131,8 +137,13 @@ class EmojiCompatFont(
val fontRegex = "$name(\\d+(\\.\\d+)*)?\\.ttf".toPattern()
val ttfFilter = FilenameFilter { _, name: String -> name.endsWith(".ttf") }
val foundFontFiles = directory.listFiles(ttfFilter).orEmpty()
Log.d(TAG, String.format("loadExistingFontFiles: %d other font files found",
foundFontFiles.size))
Log.d(
TAG,
String.format(
"loadExistingFontFiles: %d other font files found",
foundFontFiles.size
)
)
return foundFontFiles.map { file ->
val matcher = fontRegex.matcher(file.name)
@ -170,8 +181,10 @@ class EmojiCompatFont(
}
}
fun downloadFontFile(context: Context,
okHttpClient: OkHttpClient): Observable<Float> {
fun downloadFontFile(
context: Context,
okHttpClient: OkHttpClient
): Observable<Float> {
return Observable.create { emitter: ObservableEmitter<Float> ->
// It is possible (and very likely) that the file does not exist yet
val downloadFile = getFontFile(context)!!
@ -180,7 +193,7 @@ class EmojiCompatFont(
downloadFile.createNewFile()
}
val request = Request.Builder().url(url)
.build()
.build()
val sink = downloadFile.sink().buffer()
var source: Source? = null
@ -197,7 +210,7 @@ class EmojiCompatFont(
while (!emitter.isDisposed) {
sink.write(source, CHUNK_SIZE)
progress += CHUNK_SIZE.toFloat()
if(size > 0) {
if (size > 0) {
emitter.onNext(progress / size)
} else {
emitter.onNext(-1f)
@ -213,7 +226,6 @@ class EmojiCompatFont(
Log.e(TAG, "Downloading $url failed. Status code: ${response.code}")
emitter.tryOnError(Exception())
}
} catch (ex: IOException) {
Log.e(TAG, "Downloading $url failed.", ex)
downloadFile.deleteIfExists()
@ -228,10 +240,8 @@ class EmojiCompatFont(
emitter.onComplete()
}
}
}
.subscribeOn(Schedulers.io())
.subscribeOn(Schedulers.io())
}
/**
@ -256,32 +266,37 @@ class EmojiCompatFont(
private const val CHUNK_SIZE = 4096L
// The system font gets some special behavior...
val SYSTEM_DEFAULT = EmojiCompatFont("system-default",
"System Default",
R.string.caption_systememoji,
R.drawable.ic_emoji_34dp,
"",
"0")
val BLOBMOJI = EmojiCompatFont("Blobmoji",
"Blobmoji",
R.string.caption_blobmoji,
R.drawable.ic_blobmoji,
"https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
"12.0.0"
val SYSTEM_DEFAULT = EmojiCompatFont(
"system-default",
"System Default",
R.string.caption_systememoji,
R.drawable.ic_emoji_34dp,
"",
"0"
)
val TWEMOJI = EmojiCompatFont("Twemoji",
"Twemoji",
R.string.caption_twemoji,
R.drawable.ic_twemoji,
"https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
"12.0.0"
val BLOBMOJI = EmojiCompatFont(
"Blobmoji",
"Blobmoji",
R.string.caption_blobmoji,
R.drawable.ic_blobmoji,
"https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
"12.0.0"
)
val NOTOEMOJI = EmojiCompatFont("NotoEmoji",
"Noto Emoji",
R.string.caption_notoemoji,
R.drawable.ic_notoemoji,
"https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf",
"11.0.0"
val TWEMOJI = EmojiCompatFont(
"Twemoji",
"Twemoji",
R.string.caption_twemoji,
R.drawable.ic_twemoji,
"https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
"12.0.0"
)
val NOTOEMOJI = EmojiCompatFont(
"NotoEmoji",
"Noto Emoji",
R.string.caption_notoemoji,
R.drawable.ic_notoemoji,
"https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf",
"11.0.0"
)
/**
@ -341,11 +356,9 @@ class EmojiCompatFont(
}
private fun File.deleteIfExists() {
if(exists() && !delete()) {
if (exists() && !delete()) {
Log.e(TAG, "Could not delete file $this")
}
}
}
}
}

View file

@ -16,7 +16,6 @@
package com.keylesspalace.tusky.util
import android.graphics.Matrix
import com.keylesspalace.tusky.entity.Attachment.Focus
/**
@ -54,12 +53,14 @@ object FocalPointUtil {
*
* @return The matrix which correctly crops the image
*/
fun updateFocalPointMatrix(viewWidth: Float,
viewHeight: Float,
imageWidth: Float,
imageHeight: Float,
focus: Focus,
mat: Matrix) {
fun updateFocalPointMatrix(
viewWidth: Float,
viewHeight: Float,
imageWidth: Float,
imageHeight: Float,
focus: Focus,
mat: Matrix
) {
// Reset the cached matrix:
mat.reset()
@ -84,11 +85,15 @@ object FocalPointUtil {
*
* The scaling used depends on if we need a vertical of horizontal crop.
*/
fun calculateScaling(viewWidth: Float, viewHeight: Float,
imageWidth: Float, imageHeight: Float): Float {
fun calculateScaling(
viewWidth: Float,
viewHeight: Float,
imageWidth: Float,
imageHeight: Float
): Float {
return if (isVerticalCrop(viewWidth, viewHeight, imageWidth, imageHeight)) {
viewWidth / imageWidth
} else { // horizontal crop:
} else { // horizontal crop:
viewHeight / imageHeight
}
}
@ -96,8 +101,12 @@ object FocalPointUtil {
/**
* Return true if we need a vertical crop, false for a horizontal crop.
*/
fun isVerticalCrop(viewWidth: Float, viewHeight: Float,
imageWidth: Float, imageHeight: Float): Boolean {
fun isVerticalCrop(
viewWidth: Float,
viewHeight: Float,
imageWidth: Float,
imageHeight: Float
): Boolean {
val viewRatio = viewWidth / viewHeight
val imageRatio = imageWidth / imageHeight
@ -135,8 +144,12 @@ object FocalPointUtil {
* the image. So it won't put the very edge of the image in center, because that would
* leave part of the view empty.
*/
fun focalOffset(view: Float, image: Float,
scale: Float, focal: Float): Float {
fun focalOffset(
view: Float,
image: Float,
scale: Float,
focal: Float
): Float {
// The fraction of the image that will be in view:
val inView = view / (scale * image)
var offset = 0f

View file

@ -11,41 +11,38 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.keylesspalace.tusky.R
private val centerCropTransformation = CenterCrop()
fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boolean) {
if (url.isNullOrBlank()) {
Glide.with(imageView)
.load(R.drawable.avatar_default)
.into(imageView)
.load(R.drawable.avatar_default)
.into(imageView)
} else {
if (animate) {
Glide.with(imageView)
.load(url)
.transform(
centerCropTransformation,
RoundedCorners(radius)
)
.placeholder(R.drawable.avatar_default)
.into(imageView)
.load(url)
.transform(
centerCropTransformation,
RoundedCorners(radius)
)
.placeholder(R.drawable.avatar_default)
.into(imageView)
} else {
Glide.with(imageView)
.asBitmap()
.load(url)
.transform(
centerCropTransformation,
RoundedCorners(radius)
)
.placeholder(R.drawable.avatar_default)
.into(imageView)
.asBitmap()
.load(url)
.transform(
centerCropTransformation,
RoundedCorners(radius)
)
.placeholder(R.drawable.avatar_default)
.into(imageView)
}
}
}
fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable {
return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f))
}
}

View file

@ -32,7 +32,7 @@ class ListStatusAccessibilityDelegate(
private val statusProvider: StatusProvider
) : RecyclerViewAccessibilityDelegate(recyclerView) {
private val a11yManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE)
as AccessibilityManager
as AccessibilityManager
override fun getItemDelegate(): AccessibilityDelegateCompat = itemDelegate
@ -92,11 +92,11 @@ class ListStatusAccessibilityDelegate(
info.addAction(moreAction)
}
}
override fun performAccessibilityAction(
host: View, action: Int,
host: View,
action: Int,
args: Bundle?
): Boolean {
val pos = recyclerView.getChildAdapterPosition(host)
@ -170,7 +170,6 @@ class ListStatusAccessibilityDelegate(
return true
}
private fun showLinksDialog(host: View) {
val status = getStatus(host) as? StatusViewData.Concrete ?: return
val links = getLinks(status).toList()
@ -228,7 +227,6 @@ class ListStatusAccessibilityDelegate(
}
}
private fun getLinks(status: StatusViewData.Concrete): Sequence<LinkSpanInfo> {
val content = status.content
return if (content is Spannable) {
@ -268,7 +266,6 @@ class ListStatusAccessibilityDelegate(
a11yManager.interrupt()
}
private fun isHashtag(text: CharSequence) = text.startsWith("#")
private val collapseCwAction = AccessibilityActionCompat(
@ -357,4 +354,4 @@ class ListStatusAccessibilityDelegate(
)
private data class LinkSpanInfo(val text: String, val link: String)
}
}

View file

@ -17,9 +17,8 @@
package com.keylesspalace.tusky.util
import java.util.LinkedHashSet
import java.util.ArrayList
import java.util.LinkedHashSet
/**
* @return true if list is null or else return list.isEmpty()
@ -56,4 +55,4 @@ inline fun <T> List<T>.replacedFirstWhich(replacement: T, predicate: (T) -> Bool
inline fun <reified R> Iterable<*>.firstIsInstanceOrNull(): R? {
return firstOrNull { it is R }?.let { it as R }
}
}

View file

@ -15,17 +15,21 @@
package com.keylesspalace.tusky.util
import androidx.lifecycle.*
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.LiveDataReactiveStreams
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
inline fun <X, Y> LiveData<X>.map(crossinline mapFunction: (X) -> Y): LiveData<Y> =
Transformations.map(this) { input -> mapFunction(input) }
Transformations.map(this) { input -> mapFunction(input) }
inline fun <X, Y> LiveData<X>.switchMap(
crossinline switchMapFunction: (X) -> LiveData<Y>
crossinline switchMapFunction: (X) -> LiveData<Y>
): LiveData<Y> = Transformations.switchMap(this) { input -> switchMapFunction(input) }
inline fun <X> LiveData<X>.filter(crossinline predicate: (X) -> Boolean): LiveData<X> {
@ -39,17 +43,17 @@ inline fun <X> LiveData<X>.filter(crossinline predicate: (X) -> Boolean): LiveDa
}
fun LifecycleOwner.withLifecycleContext(body: LifecycleContext.() -> Unit) =
LifecycleContext(this).apply(body)
LifecycleContext(this).apply(body)
class LifecycleContext(val lifecycleOwner: LifecycleOwner) {
inline fun <T> LiveData<T>.observe(crossinline observer: (T) -> Unit) =
this.observe(lifecycleOwner, Observer { observer(it) })
this.observe(lifecycleOwner, Observer { observer(it) })
/**
* Just hold a subscription,
*/
fun <T> LiveData<T>.subscribe() =
this.observe(lifecycleOwner, Observer { })
this.observe(lifecycleOwner, Observer { })
}
/**
@ -90,5 +94,5 @@ fun <A, B, R> combineOptionalLiveData(a: LiveData<A>, b: LiveData<B>, combiner:
fun <T> Single<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this.toFlowable())
fun <T> Observable<T>.toLiveData(
backpressureStrategy: BackpressureStrategy = BackpressureStrategy.LATEST
) = LiveDataReactiveStreams.fromPublisher(this.toFlowable(BackpressureStrategy.LATEST))
backpressureStrategy: BackpressureStrategy = BackpressureStrategy.LATEST
) = LiveDataReactiveStreams.fromPublisher(this.toFlowable(BackpressureStrategy.LATEST))

View file

@ -19,7 +19,7 @@ import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import androidx.preference.PreferenceManager
import java.util.*
import java.util.Locale
class LocaleManager(context: Context) {

View file

@ -22,11 +22,13 @@ import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import androidx.annotation.Px
import androidx.exifinterface.media.ExifInterface
import android.util.Log
import java.io.*
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
@ -46,7 +48,7 @@ const val MEDIA_SIZE_UNKNOWN = -1L
* @return the size of the media in bytes or {@link MediaUtils#MEDIA_SIZE_UNKNOWN}
*/
fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long {
if(uri == null) {
if (uri == null) {
return MEDIA_SIZE_UNKNOWN
}
@ -165,8 +167,10 @@ fun reorientBitmap(bitmap: Bitmap?, orientation: Int): Bitmap? {
}
return try {
val result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width,
bitmap.height, matrix, true)
val result = Bitmap.createBitmap(
bitmap, 0, 0, bitmap.width,
bitmap.height, matrix, true
)
if (!bitmap.sameAs(result)) {
bitmap.recycle()
}
@ -210,7 +214,7 @@ fun deleteStaleCachedMedia(mediaDirectory: File?) {
twentyfourHoursAgo.add(Calendar.HOUR, -24)
val unixTime = twentyfourHoursAgo.timeInMillis
val files = mediaDirectory.listFiles{ file -> unixTime > file.lastModified() && file.name.contains(MEDIA_TEMP_PREFIX) }
val files = mediaDirectory.listFiles { file -> unixTime > file.lastModified() && file.name.contains(MEDIA_TEMP_PREFIX) }
if (files == null || files.isEmpty()) {
// Nothing to do
return

View file

@ -24,11 +24,12 @@ enum class Status {
@Suppress("DataClassPrivateConstructor")
data class NetworkState private constructor(
val status: Status,
val msg: String? = null) {
val status: Status,
val msg: String? = null
) {
companion object {
val LOADED = NetworkState(Status.SUCCESS)
val LOADING = NetworkState(Status.RUNNING)
fun error(msg: String?) = NetworkState(Status.FAILED, msg)
}
}
}

View file

@ -42,4 +42,4 @@ fun deserialize(data: String?): Set<Notification.Type> {
}
}
return ret
}
}

View file

@ -49,4 +49,4 @@ class PickMediaFiles : ActivityResultContract<Boolean, List<Uri>>() {
}
return emptyList()
}
}
}

View file

@ -6,8 +6,9 @@ class Loading<T> (override val data: T? = null) : Resource<T>(data)
class Success<T> (override val data: T? = null) : Resource<T>(data)
class Error<T> (override val data: T? = null,
val errorMessage: String? = null,
var consumed: Boolean = false,
val cause: Throwable? = null
): Resource<T>(data)
class Error<T> (
override val data: T? = null,
val errorMessage: String? = null,
var consumed: Boolean = false,
val cause: Throwable? = null
) : Resource<T>(data)

View file

@ -6,9 +6,9 @@ import android.net.Uri
import com.keylesspalace.tusky.R
fun shouldRickRoll(context: Context, domain: String) =
context.resources.getStringArray(R.array.rick_roll_domains).any { candidate ->
domain.equals(candidate, true) || domain.endsWith(".$candidate", true)
}
context.resources.getStringArray(R.array.rick_roll_domains).any { candidate ->
domain.equals(candidate, true) || domain.endsWith(".$candidate", true)
}
fun rickRoll(context: Context) {
val uri = Uri.parse(context.getString(R.string.rick_roll_url))

View file

@ -15,4 +15,4 @@ open class RxAwareViewModel : ViewModel() {
super.onCleared()
disposables.clear()
}
}
}

View file

@ -43,17 +43,17 @@ fun updateShortcut(context: Context, account: AccountEntity) {
val bmp = if (TextUtils.isEmpty(account.profilePictureUrl)) {
Glide.with(context)
.asBitmap()
.load(R.drawable.avatar_default)
.submit(innerSize, innerSize)
.get()
.asBitmap()
.load(R.drawable.avatar_default)
.submit(innerSize, innerSize)
.get()
} else {
Glide.with(context)
.asBitmap()
.load(account.profilePictureUrl)
.error(R.drawable.avatar_default)
.submit(innerSize, innerSize)
.get()
.asBitmap()
.load(account.profilePictureUrl)
.error(R.drawable.avatar_default)
.submit(innerSize, innerSize)
.get()
}
// inset the loaded bitmap inside a 108dp transparent canvas so it looks good as adaptive icon
@ -65,10 +65,10 @@ fun updateShortcut(context: Context, account: AccountEntity) {
val icon = IconCompat.createWithAdaptiveBitmap(outBmp)
val person = Person.Builder()
.setIcon(icon)
.setName(account.displayName)
.setKey(account.identifier)
.build()
.setIcon(icon)
.setName(account.displayName)
.setKey(account.identifier)
.build()
// This intent will be sent when the user clicks on one of the launcher shortcuts. Intent from share sheet will be different
val intent = Intent(context, MainActivity::class.java).apply {
@ -78,26 +78,22 @@ fun updateShortcut(context: Context, account: AccountEntity) {
}
val shortcutInfo = ShortcutInfoCompat.Builder(context, account.id.toString())
.setIntent(intent)
.setCategories(setOf("com.keylesspalace.tusky.Share"))
.setShortLabel(account.displayName)
.setPerson(person)
.setLongLived(true)
.setIcon(icon)
.build()
.setIntent(intent)
.setCategories(setOf("com.keylesspalace.tusky.Share"))
.setShortLabel(account.displayName)
.setPerson(person)
.setLongLived(true)
.setIcon(icon)
.build()
ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo))
}
.subscribeOn(Schedulers.io())
.onErrorReturnItem(false)
.subscribe()
.subscribeOn(Schedulers.io())
.onErrorReturnItem(false)
.subscribe()
}
fun removeShortcut(context: Context, account: AccountEntity) {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(account.id.toString()))
}
}

View file

@ -35,10 +35,10 @@ private const val LENGTH_DEFAULT = 500
* be hidden will not be enough to justify the operation.
*
* @param message The message to trim.
* @return Whether the message should be trimmed or not.
* @return Whether the message should be trimmed or not.
*/
fun shouldTrimStatus(message: Spanned): Boolean {
return message.isNotEmpty() && LENGTH_DEFAULT.toFloat() / message.length < 0.75
return message.isNotEmpty() && LENGTH_DEFAULT.toFloat() / message.length < 0.75
}
/**
@ -53,59 +53,59 @@ fun shouldTrimStatus(message: Spanned): Boolean {
* </ul>
*/
object SmartLengthInputFilter : InputFilter {
/** {@inheritDoc} */
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
// Code originally imported from InputFilter.LengthFilter but heavily customized and converted to Kotlin.
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175
/** {@inheritDoc} */
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
// Code originally imported from InputFilter.LengthFilter but heavily customized and converted to Kotlin.
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175
val sourceLength = source.length
var keep = LENGTH_DEFAULT - (dest.length - (dend - dstart))
if (keep <= 0) return ""
if (keep >= end - start) return null // Keep original
val sourceLength = source.length
var keep = LENGTH_DEFAULT - (dest.length - (dend - dstart))
if (keep <= 0) return ""
if (keep >= end - start) return null // Keep original
keep += start
keep += start
// Skip trimming if the ratio doesn't warrant it
if (keep.toDouble() / sourceLength > 0.75) return null
// Skip trimming if the ratio doesn't warrant it
if (keep.toDouble() / sourceLength > 0.75) return null
// Enable trimming at the end of the closest word if possible
if (source[keep].isLetterOrDigit()) {
var boundary: Int
// Enable trimming at the end of the closest word if possible
if (source[keep].isLetterOrDigit()) {
var boundary: Int
// Android N+ offer a clone of the ICU APIs in Java for better internationalization and
// unicode support. Using the ICU version of BreakIterator grants better support for
// those without having to add the ICU4J library at a minimum Api trade-off.
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
val iterator = android.icu.text.BreakIterator.getWordInstance()
iterator.setText(source.toString())
boundary = iterator.following(keep)
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep)
} else {
val iterator = java.text.BreakIterator.getWordInstance()
iterator.setText(source.toString())
boundary = iterator.following(keep)
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep)
}
// Android N+ offer a clone of the ICU APIs in Java for better internationalization and
// unicode support. Using the ICU version of BreakIterator grants better support for
// those without having to add the ICU4J library at a minimum Api trade-off.
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
val iterator = android.icu.text.BreakIterator.getWordInstance()
iterator.setText(source.toString())
boundary = iterator.following(keep)
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep)
} else {
val iterator = java.text.BreakIterator.getWordInstance()
iterator.setText(source.toString())
boundary = iterator.following(keep)
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep)
}
keep = boundary
} else {
keep = boundary
} else {
// If no runway is allowed simply remove whitespaces if present
while(source[keep - 1].isWhitespace()) {
--keep
if (keep == start) return ""
}
}
// If no runway is allowed simply remove whitespaces if present
while (source[keep - 1].isWhitespace()) {
--keep
if (keep == start) return ""
}
}
if (source[keep - 1].isHighSurrogate()) {
--keep
if (keep == start) return ""
}
if (source[keep - 1].isHighSurrogate()) {
--keep
if (keep == start) return ""
}
return if (source is Spanned) {
SpannableStringBuilder(source, start, keep).append("")
} else {
"${source.subSequence(start, keep)}"
}
}
}
return if (source is Spanned) {
SpannableStringBuilder(source, start, keep).append("")
} else {
"${source.subSequence(start, keep)}"
}
}
}

View file

@ -49,13 +49,17 @@ private class FindCharsResult {
var end: Int = -1
}
private class PatternFinder(val searchCharacter: Char, regex: String, val searchPrefixWidth: Int,
val prefixValidator: (Int) -> Boolean) {
private class PatternFinder(
val searchCharacter: Char,
regex: String,
val searchPrefixWidth: Int,
val prefixValidator: (Int) -> Boolean
) {
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
}
private fun <T> clearSpans(text: Spannable, spanClass: Class<T>) {
for(span in text.getSpans(0, text.length, spanClass)) {
for (span in text.getSpans(0, text.length, spanClass)) {
text.removeSpan(span)
}
}
@ -66,14 +70,18 @@ private fun findPattern(string: String, fromIndex: Int): FindCharsResult {
val c = string[i]
for (matchType in FoundMatchType.values()) {
val finder = finders[matchType]
if (finder!!.searchCharacter == c
&& ((i - fromIndex) < finder.searchPrefixWidth ||
finder.prefixValidator(string.codePointAt(i - finder.searchPrefixWidth)))) {
if (finder!!.searchCharacter == c &&
(
(i - fromIndex) < finder.searchPrefixWidth ||
finder.prefixValidator(string.codePointAt(i - finder.searchPrefixWidth))
)
) {
result.matchType = matchType
result.start = max(0, i - finder.searchPrefixWidth)
findEndOfPattern(string, result, finder.pattern)
if (result.start + finder.searchPrefixWidth <= i + 1 && // The found result is actually triggered by the correct search character
result.end >= result.start) { // ...and we actually found a valid result
result.end >= result.start
) { // ...and we actually found a valid result
return result
}
}
@ -92,7 +100,8 @@ private fun findEndOfPattern(string: String, result: FindCharsResult, pattern: P
FoundMatchType.TAG -> {
if (isValidForTagPrefix(string.codePointAt(result.start))) {
if (string[result.start] != '#' ||
(string[result.start] == '#' && string[result.start + 1] == '#')) {
(string[result.start] == '#' && string[result.start + 1] == '#')
) {
++result.start
}
}
@ -116,7 +125,7 @@ private fun findEndOfPattern(string: String, result: FindCharsResult, pattern: P
}
private fun getSpan(matchType: FoundMatchType, string: String, colour: Int, start: Int, end: Int): CharacterStyle {
return when(matchType) {
return when (matchType) {
FoundMatchType.HTTP_URL -> NoUnderlineURLSpan(string.substring(start, end))
FoundMatchType.HTTPS_URL -> NoUnderlineURLSpan(string.substring(start, end))
else -> ForegroundColorSpan(colour)
@ -149,13 +158,15 @@ fun highlightSpans(text: Spannable, colour: Int) {
private fun isWordCharacters(codePoint: Int): Boolean {
return (codePoint in 0x30..0x39) || // [0-9]
(codePoint in 0x41..0x5a) || // [A-Z]
(codePoint == 0x5f) || // _
(codePoint in 0x61..0x7a) // [a-z]
(codePoint in 0x41..0x5a) || // [A-Z]
(codePoint == 0x5f) || // _
(codePoint in 0x61..0x7a) // [a-z]
}
private fun isValidForTagPrefix(codePoint: Int): Boolean {
return !(isWordCharacters(codePoint) || // \w
return !(
isWordCharacters(codePoint) || // \w
(codePoint == 0x2f) || // /
(codePoint == 0x29)) // )
(codePoint == 0x29)
) // )
}

View file

@ -1,22 +1,22 @@
package com.keylesspalace.tusky.util
data class StatusDisplayOptions(
@get:JvmName("animateAvatars")
val animateAvatars: Boolean,
@get:JvmName("mediaPreviewEnabled")
val mediaPreviewEnabled: Boolean,
@get:JvmName("useAbsoluteTime")
val useAbsoluteTime: Boolean,
@get:JvmName("showBotOverlay")
val showBotOverlay: Boolean,
@get:JvmName("useBlurhash")
val useBlurhash: Boolean,
@get:JvmName("cardViewMode")
val cardViewMode: CardViewMode,
@get:JvmName("confirmReblogs")
val confirmReblogs: Boolean,
@get:JvmName("hideStats")
val hideStats: Boolean,
@get:JvmName("animateEmojis")
val animateEmojis: Boolean
)
@get:JvmName("animateAvatars")
val animateAvatars: Boolean,
@get:JvmName("mediaPreviewEnabled")
val mediaPreviewEnabled: Boolean,
@get:JvmName("useAbsoluteTime")
val useAbsoluteTime: Boolean,
@get:JvmName("showBotOverlay")
val showBotOverlay: Boolean,
@get:JvmName("useBlurhash")
val useBlurhash: Boolean,
@get:JvmName("cardViewMode")
val cardViewMode: CardViewMode,
@get:JvmName("confirmReblogs")
val confirmReblogs: Boolean,
@get:JvmName("hideStats")
val hideStats: Boolean,
@get:JvmName("animateEmojis")
val animateEmojis: Boolean
)

View file

@ -34,7 +34,8 @@ import com.keylesspalace.tusky.viewdata.buildDescription
import com.keylesspalace.tusky.viewdata.calculatePercent
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
import kotlin.math.min
class StatusViewHelper(private val itemView: View) {
@ -47,25 +48,28 @@ class StatusViewHelper(private val itemView: View) {
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
fun setMediasPreview(
statusDisplayOptions: StatusDisplayOptions,
attachments: List<Attachment>,
sensitive: Boolean,
previewListener: MediaPreviewListener,
showingContent: Boolean,
mediaPreviewHeight: Int) {
statusDisplayOptions: StatusDisplayOptions,
attachments: List<Attachment>,
sensitive: Boolean,
previewListener: MediaPreviewListener,
showingContent: Boolean,
mediaPreviewHeight: Int
) {
val context = itemView.context
val mediaPreviews = arrayOf<MediaPreviewImageView>(
itemView.findViewById(R.id.status_media_preview_0),
itemView.findViewById(R.id.status_media_preview_1),
itemView.findViewById(R.id.status_media_preview_2),
itemView.findViewById(R.id.status_media_preview_3))
itemView.findViewById(R.id.status_media_preview_0),
itemView.findViewById(R.id.status_media_preview_1),
itemView.findViewById(R.id.status_media_preview_2),
itemView.findViewById(R.id.status_media_preview_3)
)
val mediaOverlays = arrayOf<ImageView>(
itemView.findViewById(R.id.status_media_overlay_0),
itemView.findViewById(R.id.status_media_overlay_1),
itemView.findViewById(R.id.status_media_overlay_2),
itemView.findViewById(R.id.status_media_overlay_3))
itemView.findViewById(R.id.status_media_overlay_0),
itemView.findViewById(R.id.status_media_overlay_1),
itemView.findViewById(R.id.status_media_overlay_2),
itemView.findViewById(R.id.status_media_overlay_3)
)
val sensitiveMediaWarning = itemView.findViewById<TextView>(R.id.status_sensitive_media_warning)
val sensitiveMediaShow = itemView.findViewById<View>(R.id.status_sensitive_media_button)
@ -85,7 +89,6 @@ class StatusViewHelper(private val itemView: View) {
return
}
val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(context, R.attr.colorBackgroundAccent))
val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS)
@ -105,9 +108,9 @@ class StatusViewHelper(private val itemView: View) {
if (TextUtils.isEmpty(previewUrl)) {
Glide.with(mediaPreviews[i])
.load(mediaPreviewUnloaded)
.centerInside()
.into(mediaPreviews[i])
.load(mediaPreviewUnloaded)
.centerInside()
.into(mediaPreviews[i])
} else {
val placeholder = if (attachment.blurhash != null)
decodeBlurHash(context, attachment.blurhash)
@ -119,19 +122,19 @@ class StatusViewHelper(private val itemView: View) {
mediaPreviews[i].setFocalPoint(focus)
Glide.with(mediaPreviews[i])
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.addListener(mediaPreviews[i])
.into(mediaPreviews[i])
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.addListener(mediaPreviews[i])
.into(mediaPreviews[i])
} else {
mediaPreviews[i].removeFocalPoint()
Glide.with(mediaPreviews[i])
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.into(mediaPreviews[i])
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.into(mediaPreviews[i])
}
} else {
mediaPreviews[i].removeFocalPoint()
@ -145,8 +148,9 @@ class StatusViewHelper(private val itemView: View) {
}
val type = attachment.type
if (showingContent
&& (type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) {
if (showingContent &&
(type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)
) {
mediaOverlays[i].visibility = View.VISIBLE
} else {
mediaOverlays[i].visibility = View.GONE
@ -170,7 +174,7 @@ class StatusViewHelper(private val itemView: View) {
sensitiveMediaWarning.visibility = View.GONE
sensitiveMediaShow.visibility = View.GONE
} else {
sensitiveMediaWarning.text = if (sensitive) {
sensitiveMediaWarning.text = if (sensitive) {
context.getString(R.string.status_sensitive_media_title)
} else {
context.getString(R.string.status_media_hidden_title)
@ -182,15 +186,19 @@ class StatusViewHelper(private val itemView: View) {
previewListener.onContentHiddenChange(false)
v.visibility = View.GONE
sensitiveMediaWarning.visibility = View.VISIBLE
setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener,
false, mediaPreviewHeight)
setMediasPreview(
statusDisplayOptions, attachments, sensitive, previewListener,
false, mediaPreviewHeight
)
}
sensitiveMediaWarning.setOnClickListener { v ->
previewListener.onContentHiddenChange(true)
v.visibility = View.GONE
sensitiveMediaShow.visibility = View.VISIBLE
setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener,
true, mediaPreviewHeight)
setMediasPreview(
statusDisplayOptions, attachments, sensitive, previewListener,
true, mediaPreviewHeight
)
}
}
@ -200,8 +208,12 @@ class StatusViewHelper(private val itemView: View) {
}
}
private fun setMediaLabel(mediaLabel: TextView, attachments: List<Attachment>, sensitive: Boolean,
listener: MediaPreviewListener) {
private fun setMediaLabel(
mediaLabel: TextView,
attachments: List<Attachment>,
sensitive: Boolean,
listener: MediaPreviewListener
) {
if (attachments.isEmpty()) {
mediaLabel.visibility = View.GONE
return
@ -245,10 +257,11 @@ class StatusViewHelper(private val itemView: View) {
fun setupPollReadonly(poll: PollViewData?, emojis: List<Emoji>, statusDisplayOptions: StatusDisplayOptions) {
val pollResults = listOf<TextView>(
itemView.findViewById(R.id.status_poll_option_result_0),
itemView.findViewById(R.id.status_poll_option_result_1),
itemView.findViewById(R.id.status_poll_option_result_2),
itemView.findViewById(R.id.status_poll_option_result_3))
itemView.findViewById(R.id.status_poll_option_result_0),
itemView.findViewById(R.id.status_poll_option_result_1),
itemView.findViewById(R.id.status_poll_option_result_2),
itemView.findViewById(R.id.status_poll_option_result_3)
)
val pollDescription = itemView.findViewById<TextView>(R.id.status_poll_description)
@ -260,7 +273,6 @@ class StatusViewHelper(private val itemView: View) {
} else {
val timestamp = System.currentTimeMillis()
setupPollResult(poll, emojis, pollResults, statusDisplayOptions.animateEmojis)
pollDescription.visibility = View.VISIBLE
@ -271,7 +283,7 @@ class StatusViewHelper(private val itemView: View) {
private fun getPollInfoText(timestamp: Long, poll: PollViewData, pollDescription: TextView, useAbsoluteTime: Boolean): CharSequence {
val context = pollDescription.context
val votesText = if(poll.votersCount == null) {
val votesText = if (poll.votersCount == null) {
val votes = NumberFormat.getNumberInstance().format(poll.votesCount.toLong())
context.resources.getQuantityString(R.plurals.poll_info_votes, poll.votesCount, votes)
} else {
@ -291,7 +303,6 @@ class StatusViewHelper(private val itemView: View) {
return context.getString(R.string.poll_info_format, votesText, pollDurationInfo)
}
private fun setupPollResult(poll: PollViewData, emojis: List<Emoji>, pollResults: List<TextView>, animateEmojis: Boolean) {
val options = poll.options
@ -306,7 +317,6 @@ class StatusViewHelper(private val itemView: View) {
val level = percent * 100
pollResults[i].background.level = level
} else {
pollResults[i].visibility = View.GONE
}
@ -329,4 +339,4 @@ class StatusViewHelper(private val itemView: View) {
val COLLAPSE_INPUT_FILTER = arrayOf<InputFilter>(SmartLengthInputFilter)
val NO_INPUT_FILTER = arrayOfNulls<InputFilter>(0)
}
}
}

View file

@ -3,8 +3,7 @@
package com.keylesspalace.tusky.util
import android.text.Spanned
import java.util.*
import java.util.Random
private const val POSSIBLE_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@ -30,7 +29,6 @@ fun String.inc(): String {
return String(builder)
}
/**
* "Decrement" string so that during sorting it's smaller than [this].
*/
@ -97,4 +95,4 @@ fun Spanned.trimTrailingWhitespace(): Spanned {
*/
fun CharSequence.unicodeWrap(): String {
return "\u2068${this}\u2069"
}
}

View file

@ -16,35 +16,35 @@ import kotlin.reflect.KProperty
*/
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
crossinline bindingInflater: (LayoutInflater) -> T
crossinline bindingInflater: (LayoutInflater) -> T
) = lazy(LazyThreadSafetyMode.NONE) {
bindingInflater(layoutInflater)
}
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var binding: T? = null
init {
fragment.lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(
fragment,
{ t ->
t?.lifecycle?.addObserver(
object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
}
)
object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(
fragment,
{ t ->
t?.lifecycle?.addObserver(
object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
}
)
}
)
}
)
}
}
)
}
@ -64,4 +64,4 @@ class FragmentViewBindingDelegate<T : ViewBinding>(
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
FragmentViewBindingDelegate(this, viewBindingFactory)

View file

@ -20,8 +20,6 @@ import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.viewdata.NotificationViewData
import com.keylesspalace.tusky.viewdata.StatusViewData
import com.keylesspalace.tusky.viewdata.toViewData
import java.util.*
@JvmName("statusToViewData")
fun Status.toViewData(
@ -50,4 +48,4 @@ fun Notification.toViewData(
this.account,
this.status?.toViewData(alwaysShowSensitiveData, alwaysOpenSpoiler)
)
}
}

View file

@ -45,7 +45,8 @@ open class DefaultTextWatcher : TextWatcher {
}
inline fun EditText.onTextChanged(
crossinline callback: (s: CharSequence, start: Int, before: Int, count: Int) -> Unit) {
crossinline callback: (s: CharSequence, start: Int, before: Int, count: Int) -> Unit
) {
addTextChangedListener(object : DefaultTextWatcher() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
callback(s, start, before, count)
@ -54,10 +55,11 @@ inline fun EditText.onTextChanged(
}
inline fun EditText.afterTextChanged(
crossinline callback: (s: Editable) -> Unit) {
crossinline callback: (s: Editable) -> Unit
) {
addTextChangedListener(object : DefaultTextWatcher() {
override fun afterTextChanged(s: Editable) {
callback(s)
}
})
}
}