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:
parent
955267199e
commit
16ffcca748
227 changed files with 3933 additions and 3371 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ enum class CardViewMode {
|
|||
NONE,
|
||||
FULL_WIDTH,
|
||||
INDENTED
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?) {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,4 +44,4 @@ sealed class Either<out L, out R> {
|
|||
Right(mapper(this.asRight()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,4 +42,4 @@ fun deserialize(data: String?): Set<Notification.Type> {
|
|||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,4 +49,4 @@ class PickMediaFiles : ActivityResultContract<Boolean, List<Uri>>() {
|
|||
}
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -15,4 +15,4 @@ open class RxAwareViewModel : ViewModel() {
|
|||
super.onCleared()
|
||||
disposables.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}…"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
) // )
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue