Added SVG emoji support
Now emojis in SVG format will be decoded and showed the same as a PNG emoji. Also those emojis are cached, so there are not additional network calls when requesting a SVG emoji.
This commit is contained in:
parent
7f84ec5ea9
commit
a63c7dc96f
8 changed files with 324 additions and 47 deletions
|
@ -217,8 +217,10 @@ dependencies {
|
|||
implementation(ApplicationLibs.Dagger.daggerSupport)
|
||||
|
||||
implementation(ApplicationLibs.Glide.glide)
|
||||
implementation(ApplicationLibs.Glide.glideOkhttp)
|
||||
kapt(ApplicationLibs.Glide.glideCompiler)
|
||||
implementation(ApplicationLibs.Glide.glideImage)
|
||||
implementation(ApplicationLibs.Glide.glideImageViewFactory)
|
||||
implementation(ApplicationLibs.Glide.glideOkhttp)
|
||||
|
||||
implementation(ApplicationLibs.Google.flexbox)
|
||||
implementation(ApplicationLibs.Google.exoplayer)
|
||||
|
@ -244,14 +246,13 @@ dependencies {
|
|||
implementation(ApplicationLibs.acraMail)
|
||||
implementation(ApplicationLibs.acraNotification)
|
||||
implementation(ApplicationLibs.androidImageCropper)
|
||||
implementation(ApplicationLibs.androidSvg)
|
||||
implementation(ApplicationLibs.autodispose)
|
||||
implementation(ApplicationLibs.autodisposeAndroidArchComp)
|
||||
implementation(ApplicationLibs.bigImageViewer)
|
||||
implementation(ApplicationLibs.conscryptAndroid)
|
||||
implementation(ApplicationLibs.filemojiCompat)
|
||||
implementation(ApplicationLibs.fragmentviewbindingdelegateKt)
|
||||
implementation(ApplicationLibs.glideImage)
|
||||
implementation(ApplicationLibs.glideImageViewFactory)
|
||||
implementation(ApplicationLibs.markdownEdit)
|
||||
implementation(ApplicationLibs.materialDrawer)
|
||||
implementation(ApplicationLibs.materialDrawerIconics)
|
||||
|
|
1
husky/app/proguard-rules.pro
vendored
1
husky/app/proguard-rules.pro
vendored
|
@ -24,6 +24,7 @@
|
|||
-keepclassmembers class * extends android.app.Activity {
|
||||
public void *(android.view.View);
|
||||
}
|
||||
|
||||
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package com.keylesspalace.tusky.core.utils.image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.PictureDrawable
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.engine.Resource
|
||||
import com.bumptech.glide.load.resource.SimpleResource
|
||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
|
||||
import com.caverock.androidsvg.SVG
|
||||
|
||||
class SvgBitmapTranscoder : ResourceTranscoder<SVG, Bitmap> {
|
||||
override fun transcode(toTranscode: Resource<SVG>, options: Options): Resource<Bitmap> {
|
||||
val svg = toTranscode.get()
|
||||
val width = svg.documentWidth.toInt().takeIf { it > 0 }
|
||||
?: (svg.documentViewBox.right - svg.documentViewBox.left).toInt()
|
||||
val height = svg.documentHeight.toInt().takeIf { it > 0 }
|
||||
?: (svg.documentViewBox.bottom - svg.documentViewBox.top).toInt()
|
||||
|
||||
val picture = svg.renderToPicture(width, height)
|
||||
val drawable = PictureDrawable(picture)
|
||||
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
canvas.drawPicture(drawable.picture)
|
||||
|
||||
return SimpleResource(bitmap)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Husky -- A Pleroma client for Android
|
||||
*
|
||||
* Copyright (C) 2021 The Husky Developers
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Based on Glide's SVG decoder. Rewriten to Kotlin.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.core.utils.image
|
||||
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.ResourceDecoder
|
||||
import com.bumptech.glide.load.engine.Resource
|
||||
import com.bumptech.glide.load.resource.SimpleResource
|
||||
import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL
|
||||
import com.caverock.androidsvg.SVG
|
||||
import com.caverock.androidsvg.SVGParseException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class SvgDecoder : ResourceDecoder<InputStream, SVG> {
|
||||
|
||||
override fun handles(source: InputStream, options: Options): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun decode(
|
||||
source: InputStream,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): Resource<SVG> {
|
||||
try {
|
||||
val svg = SVG.getFromInputStream(source)
|
||||
|
||||
if(width != SIZE_ORIGINAL) {
|
||||
svg.documentWidth = width.toFloat()
|
||||
}
|
||||
|
||||
if(height != SIZE_ORIGINAL) {
|
||||
svg.documentHeight = height.toFloat()
|
||||
}
|
||||
|
||||
return SimpleResource(svg)
|
||||
} catch(ex: SVGParseException) {
|
||||
throw IOException("Cannot load SVG from stream", ex);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Husky -- A Pleroma client for Android
|
||||
*
|
||||
* Copyright (C) 2021 The Husky Developers
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some code is based on https://github.com/qoqa/glide-svg
|
||||
*
|
||||
* No copyright asserted on the source code of this repository.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.core.utils.image
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.PictureDrawable
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.engine.Resource
|
||||
import com.bumptech.glide.load.resource.SimpleResource
|
||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
|
||||
import com.caverock.androidsvg.SVG
|
||||
|
||||
class SvgDrawableTranscoder(private val context: Context) : ResourceTranscoder<SVG, Drawable> {
|
||||
|
||||
override fun transcode(toTranscode: Resource<SVG>, options: Options): Resource<Drawable> {
|
||||
return SimpleResource(
|
||||
BitmapDrawable(
|
||||
context.resources,
|
||||
getBitmap(toTranscode.get()).get()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getBitmap(svg: SVG): SimpleResource<Bitmap> {
|
||||
val width = svg.documentWidth.toInt()
|
||||
val height = svg.documentHeight.toInt()
|
||||
|
||||
val drawable = PictureDrawable(svg.renderToPicture(width, height))
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
canvas.drawPicture(drawable.picture)
|
||||
|
||||
return SimpleResource(bitmap)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Husky -- A Pleroma client for Android
|
||||
*
|
||||
* Copyright (C) 2021 The Husky Developers
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.core.utils.image;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.LibraryGlideModule;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Module for adding SVG decoding support to Glide.
|
||||
*/
|
||||
@GlideModule
|
||||
public class SvgModule extends LibraryGlideModule {
|
||||
|
||||
@Override
|
||||
public void registerComponents(
|
||||
@NonNull Context context,
|
||||
@NonNull Glide glide,
|
||||
@NonNull Registry registry) {
|
||||
registry.register(SVG.class, Drawable.class, new SvgDrawableTranscoder(context))
|
||||
.append(InputStream.class, SVG.class, new SvgDecoder());
|
||||
}
|
||||
}
|
|
@ -1,19 +1,25 @@
|
|||
/* Copyright 2020 Tusky Contributors
|
||||
/*
|
||||
* Husky -- A Pleroma client for Android
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
* Copyright (C) 2021 The Husky Developers
|
||||
* Copyright (C) 2021 Tusky Contributors
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:JvmName("CustomEmojiHelper")
|
||||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.graphics.Canvas
|
||||
|
@ -23,28 +29,35 @@ import android.text.SpannableString
|
|||
import android.text.Spanned
|
||||
import android.text.style.ReplacementSpan
|
||||
import android.view.View
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
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 com.keylesspalace.tusky.settings.PrefKeys
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.regex.Pattern
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
|
||||
/**
|
||||
* replaces emoji shortcodes in a text with EmojiSpans
|
||||
* @param text the text containing custom emojis
|
||||
* @param emojis a list of the custom emojis (nullable for backward compatibility with old mastodon instances)
|
||||
* @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, forceSmallEmoji: Boolean) : CharSequence {
|
||||
if(emojis.isNullOrEmpty())
|
||||
* Replaces emoji shortcodes in a text with EmojiSpans.
|
||||
*
|
||||
* @param text the text containing custom emojis.
|
||||
* @param emojis a list of the custom emojis (nullable for backward compatibility with old mastodon instances).
|
||||
* @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,
|
||||
forceSmallEmoji: Boolean = false
|
||||
): CharSequence {
|
||||
if(emojis.isNullOrEmpty()) {
|
||||
return this
|
||||
}
|
||||
|
||||
val builder = SpannableString.valueOf(this)
|
||||
val pm = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||
|
@ -63,24 +76,36 @@ fun CharSequence.emojify(emojis: List<Emoji>?, view: View, forceSmallEmoji: Bool
|
|||
}
|
||||
|
||||
builder.setSpan(span, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
Glide.with(view)
|
||||
.asDrawable()
|
||||
.load(url)
|
||||
.into(span.getTarget(animate))
|
||||
|
||||
var glideRequest = Glide.with(view).load(url)
|
||||
val mimetype = getMimeType(url)
|
||||
if(mimetype == MIME.SVG) {
|
||||
glideRequest = glideRequest
|
||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||
.override(512, 512)
|
||||
}
|
||||
glideRequest.into(span.getTarget(animate))
|
||||
}
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
fun CharSequence.emojify(emojis: List<Emoji>?, view: View) : CharSequence {
|
||||
fun CharSequence.emojify(emojis: List<Emoji>?, view: View): CharSequence {
|
||||
return this.emojify(emojis, view, false)
|
||||
}
|
||||
|
||||
open 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 {
|
||||
if (fm != null) {
|
||||
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 */
|
||||
val metrics = paint.fontMetricsInt
|
||||
|
@ -93,7 +118,17 @@ open class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSp
|
|||
return (paint.textSize * 2.0).toInt()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
|
||||
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()
|
||||
|
||||
|
@ -109,20 +144,23 @@ open class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSp
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
override fun scheduleDrawable(p0: Drawable, p1: Runnable, p2: Long) {
|
||||
callback?.scheduleDrawable(p0, p1, p2)
|
||||
}
|
||||
|
||||
override fun invalidateDrawable(p0: Drawable) {
|
||||
callback?.invalidateDrawable(p0)
|
||||
view.invalidate()
|
||||
|
@ -141,10 +179,15 @@ open class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSp
|
|||
}
|
||||
}
|
||||
|
||||
class SmallEmojiSpan(viewWeakReference: WeakReference<View>)
|
||||
: EmojiSpan(viewWeakReference) {
|
||||
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
|
||||
if (fm != null) {
|
||||
class SmallEmojiSpan(viewWeakReference: WeakReference<View>) : EmojiSpan(viewWeakReference) {
|
||||
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 */
|
||||
val metrics = paint.fontMetricsInt
|
||||
|
@ -157,3 +200,32 @@ class SmallEmojiSpan(viewWeakReference: WeakReference<View>)
|
|||
return paint.textSize.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Mimetype fron the URL.
|
||||
*
|
||||
* @return MIME - The Mimetype.
|
||||
*/
|
||||
private fun getMimeType(url: String?): MIME {
|
||||
var type: String? = null
|
||||
val extension = MimeTypeMap.getFileExtensionFromUrl(url)
|
||||
if(extension != null) {
|
||||
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
}
|
||||
return MIME.getMime(type)
|
||||
}
|
||||
|
||||
private enum class MIME(private val mimetype: String) {
|
||||
|
||||
NONE(""),
|
||||
SVG("image/svg+xml");
|
||||
|
||||
companion object {
|
||||
fun getMime(mime: String?): MIME {
|
||||
return when(mime) {
|
||||
SVG.mimetype -> SVG
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ object ApplicationLibs {
|
|||
private object Versions {
|
||||
const val acra = "5.8.4"
|
||||
const val androidImageCropper = "2.8.0"
|
||||
const val androidSvg = "1.4"
|
||||
const val appcompat = "1.3.1"
|
||||
const val autodispose = "1.4.0"
|
||||
const val bigImageViewer = "1.7.0"
|
||||
|
@ -105,6 +106,9 @@ object ApplicationLibs {
|
|||
object Glide {
|
||||
const val glide = "com.github.bumptech.glide:glide:${Versions.glide}"
|
||||
const val glideCompiler = "com.github.bumptech.glide:compiler:${Versions.glide}"
|
||||
const val glideImage = "com.github.piasy:GlideImageLoader:${Versions.glideImage}"
|
||||
const val glideImageViewFactory =
|
||||
"com.github.piasy:GlideImageViewFactory:${Versions.glideImage}"
|
||||
const val glideOkhttp = "com.github.bumptech.glide:okhttp3-integration:${Versions.glide}"
|
||||
}
|
||||
|
||||
|
@ -145,6 +149,7 @@ object ApplicationLibs {
|
|||
|
||||
const val acraMail = "ch.acra:acra-mail:${Versions.acra}"
|
||||
const val acraNotification = "ch.acra:acra-notification:${Versions.acra}"
|
||||
const val androidSvg = "com.caverock:androidsvg-aar:${Versions.androidSvg}"
|
||||
const val androidImageCropper =
|
||||
"com.theartofdev.edmodo:android-image-cropper:${Versions.androidImageCropper}"
|
||||
const val autodispose = "com.uber.autodispose:autodispose:${Versions.autodispose}"
|
||||
|
@ -155,9 +160,6 @@ object ApplicationLibs {
|
|||
const val filemojiCompat = "de.c1710:filemojicompat:${Versions.filemojiCompat}"
|
||||
const val fragmentviewbindingdelegateKt =
|
||||
"com.github.Zhuinden:fragmentviewbindingdelegate-kt:${Versions.fragmentviewbindingdelegateKt}"
|
||||
const val glideImage = "com.github.piasy:GlideImageLoader:${Versions.glideImage}"
|
||||
const val glideImageViewFactory =
|
||||
"com.github.piasy:GlideImageViewFactory:${Versions.glideImage}"
|
||||
const val markdownEdit = "com.github.Tunous:MarkdownEdit:${Versions.markdownEdit}"
|
||||
const val materialDrawer = "com.mikepenz:materialdrawer:${Versions.materialDrawer}"
|
||||
const val materialDrawerIconics =
|
||||
|
|
Loading…
Reference in a new issue