This commit is contained in:
lunaticbum 2025-07-24 14:54:31 +09:00
parent 668d33543c
commit 193946cadf
10 changed files with 281 additions and 115 deletions

View File

@ -13,11 +13,14 @@
}, },
"content_scripts": [ "content_scripts": [
{ {
"run_at": "document_end",
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
"js": ["messaging.js"] "js": ["messaging.js"]
} }
], ],
"permissions": [ "permissions": [
"cookies",
"<all_urls>",
"nativeMessaging", "nativeMessaging",
"nativeMessagingFromContent", "nativeMessagingFromContent",
"geckoViewAddons", "geckoViewAddons",

View File

@ -1,3 +1,5 @@
const port = browser.runtime.connectNative("browser"); const port = browser.runtime.connectNative("browser");
port.onMessage.addListener(response => { port.onMessage.addListener(response => {
var type= response["type"]; var type= response["type"];
@ -238,7 +240,13 @@ function toast(msg) {
} }
var time1 = null var time1 = null
if (port) { if (port) {
sendMessage({type: "MSG", msg:"connect prot"});
time1 = setTimeout( time1 = setTimeout(
function(){ function(){
if (location.hostname.search("clien") > -1 && document.querySelectorAll('[class^="view_top"]')) { if (location.hostname.search("clien") > -1 && document.querySelectorAll('[class^="view_top"]')) {
document.querySelectorAll('[class^="view_top"]').forEach(e => e.remove()) document.querySelectorAll('[class^="view_top"]').forEach(e => e.remove())
@ -323,9 +331,17 @@ if (port) {
var thumb = "" var thumb = ""
try { try {
thumb = e.querySelector("td").querySelector("a").getAttribute('data-link') thumb = e.querySelector("td").querySelector("a").getAttribute('data-link')
}catch (e) { try {
var sets = e.querySelector("td").querySelector("img").getAttribute('srcset')
if(sets.search(",") > -1) {
var srcSet = sets.split(",")
var newT = srcSet[srcSet.length - 1]
if (newT != null && newT.length > 5) {
thumb = newT
} }
}
} catch (e) {}
}catch (e) {}
var magnet = "" var magnet = ""
try { try {
e.querySelectorAll("td").forEach(function (e) { e.querySelectorAll("td").forEach(function (e) {
@ -351,7 +367,6 @@ if (port) {
} }
var originPage = "" var originPage = ""
try { try {
originPage = location.protocol + "//" + location.hostname + e.querySelector(".name").querySelector("a").getAttribute("href"); originPage = location.protocol + "//" + location.hostname + e.querySelector(".name").querySelector("a").getAttribute("href");
}catch (e) { }catch (e) {
@ -366,6 +381,7 @@ if (port) {
}catch (e) { }catch (e) {
} }
if (thumb.length > 0 || screenshots.length > 0) {}
datas.push({ datas.push({
"title" : title, "title" : title,
"description" : desc, "description" : desc,
@ -381,57 +397,52 @@ if (port) {
sendMessage( sendMessage(
{ {
type: "PRIVATES", type: "PRIVATES",
privates: datas privates: datas,
currentPage : location.href
} }
); );
gotoNext() gotoNext()
} }
},1500) },1500)
} }
var targetUrl = "" var targetUrl = ""
var time2 = null var time2 = null
function gotoNext() { function gotoNext() {
clearTimeout(time1) clearTimeout(time1)
try{ try{
var url = new URL(location.href);
var params = url.searchParams;
var keys = Array.from(params.keys());
console.log("targetUrl :: " + params);
console.log("targetUrl :: " + keys.length);
if (keys.length === 0 && location.href.search("page") < 0) {
targetUrl = location.protocol + "//" + location.hostname + "/?page=2"
} else {
try {
var lastValue = params.get("page");
console.log("targetUrl :: " + lastValue);
var numValue = Number(lastValue);
console.log("targetUrl :: " + numValue);
if (numValue < Number("5")) {
params.set("page", (numValue + 1).toString());
console.log("targetUrl :: " + params);
url.search = params.toString();
console.log("targetUrl :: " + url.search);
targetUrl = url.toString();
} else {
console.log("targetUrl :: ");
}
} catch (e) {
console.error(e)
}
}
console.log("targetUrl :: " + targetUrl); console.log("targetUrl :: " + targetUrl);
time2 = setTimeout(function () { time2 = setTimeout(function () {
clearTimeout(time2) clearTimeout(time2)
if (targetUrl.length > 5) { document.querySelector('[class="btn-group"]').querySelectorAll('a').forEach(function(e){
console.log("targetUrl :: " + targetUrl); if(e.hasAttribute("href") &&
location.href = targetUrl (
(e.getAttribute("href").search("page=2") > -1 && location.href.search("page") < 0) ||
(e.getAttribute("href").search("page=3") > -1 && location.href.search("page=2") > 0)
)) {
e.click()
} }
})
}, 5000); }, 5000);
} catch (e) { } catch (e) {
} }
} }
//
// if (document.readyState === "complete" || document.readyState === "interactive") {
// reportCookiesToNative();
// } else {
// window.addEventListener("DOMContentLoaded", reportCookiesToNative);
// }
// function reportCookiesToNative() {
// // 모든 쿠키를 읽어 네이티브로 전달
// sendMessage({type: "MSG", cookies: "DOMContentLoaded try to cookies send"});
// window.cookies.getAll({url: window.location.href}).then((cookies) => {
// // 쿠키 데이터를 네이티브(Android)로 전송
// sendMessage({type: "Cookies", cookies: cookies});
// });
// }

View File

@ -731,7 +731,7 @@ internal class LauncherActivity : CommonActivity() {
when(currentFragment) { when(currentFragment) {
is RssHome ->{ is RssHome ->{
if (currentFragment.binding.layoutRssSummary.root.isVisible) { if (currentFragment.binding.layoutRssSummary.root.isVisible) {
currentFragment.openGecko("") currentFragment.openGecko(rssData = currentFragment.lasted.randomOrNull())
} else { } else {
currentFragment.doNextPage() currentFragment.doNextPage()
} }

View File

@ -24,12 +24,18 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import bums.lunatic.launcher.helpers.PrefHelper import bums.lunatic.launcher.helpers.PrefHelper
import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.Blog
import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso
import kr.lunaticbum.Base import kr.lunaticbum.Base
import okhttp3.Cache
import okhttp3.OkHttpClient
import org.json.JSONObject import org.json.JSONObject
import org.mozilla.geckoview.ExperimentDelegate import org.mozilla.geckoview.ExperimentDelegate
import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings import org.mozilla.geckoview.GeckoRuntimeSettings
import java.io.File
import java.util.concurrent.TimeUnit
internal class LunaticLauncher : Application() { internal class LunaticLauncher : Application() {
@ -43,6 +49,33 @@ internal class LunaticLauncher : Application() {
appContext = this appContext = this
Base.initialize(this) Base.initialize(this)
PrefHelper.initialize(this) PrefHelper.initialize(this)
val cacheSize = 1024L * 1024 * 1024 // 60MB
val cache = Cache(File(this.filesDir, "picasso-cache"), cacheSize)
val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.addInterceptor { chain ->
val newRequest = chain.request().newBuilder()
.addHeader("Host","images.ijavtorrent.com")
.addHeader("User-Agent","Mozilla/5.0 (Android 15; Mobile; rv:139.0) Gecko/139.0 Firefox/139.0")
.addHeader("Accept","image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5")
.addHeader("Accept-Language","ko-KR,en-US;q=0.5")
.addHeader("Accept-Encoding","gzip, deflate, br, zstd")
.addHeader("Referer","https://ijavtorrent.com/")
.build()
chain.proceed(newRequest)
}
.connectTimeout(10, TimeUnit.SECONDS) // 연결 타임아웃
.readTimeout(30, TimeUnit.SECONDS) // 읽기 타임아웃
.writeTimeout(30, TimeUnit.SECONDS) // 쓰기 타임아웃
.build()
val picasso = Picasso.Builder(this)
.downloader(OkHttp3Downloader(okHttpClient))
.build()
// 앱 전체에 해당 인스턴스를 사용하려면
Picasso.setSingletonInstance(picasso)
} }

View File

@ -482,8 +482,12 @@ class GeckoWeb : BWebview {
copyToRealm(it, UpdatePolicy.ALL) copyToRealm(it, UpdatePolicy.ALL)
} }
} }
Toast.makeText(context, "Received Msg privates form ${lPortMessage.currentPage} data => ${lPortMessage.privates?.size ?: 0}", Toast.LENGTH_SHORT).show()
} }
} }
"Cookies"->{
Blog.LOGE("cookies >>> ${lPortMessage.cookies}")
}
"PagerContents" -> { "PagerContents" -> {
if (lPortMessage.contents?.isNotEmpty() == true) { if (lPortMessage.contents?.isNotEmpty() == true) {

View File

@ -19,6 +19,7 @@
package bums.lunatic.launcher.home package bums.lunatic.launcher.home
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
@ -30,9 +31,11 @@ import android.view.LayoutInflater
import android.view.PointerIcon import android.view.PointerIcon
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.NonNull import androidx.annotation.NonNull
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -64,6 +67,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
import com.google.gson.Gson import com.google.gson.Gson
import com.squareup.picasso.Picasso
import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.query import io.realm.kotlin.ext.query
import io.realm.kotlin.notifications.InitialResults import io.realm.kotlin.notifications.InitialResults
@ -76,6 +80,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
internal class RssHome : Fragment() { internal class RssHome : Fragment() {
@ -113,7 +119,7 @@ internal class RssHome : Fragment() {
): Boolean { ): Boolean {
Blog.LOGE("") Blog.LOGE("")
if (imageView){ if (imageView){
openGecko("") openGecko(rssData = lasted.randomOrNull())
} }
return true return true
} }
@ -125,7 +131,7 @@ internal class RssHome : Fragment() {
gestureDistance: Double gestureDistance: Double
): Boolean { ): Boolean {
if (imageView){ if (imageView){
openGecko("") openGecko(rssData = lasted.randomOrNull())
} }
Blog.LOGE("") Blog.LOGE("")
return true return true
@ -217,7 +223,7 @@ internal class RssHome : Fragment() {
RssDataType.REDDIT_NSFW,RssDataType.PRIVATE -> { RssDataType.REDDIT_NSFW,RssDataType.PRIVATE -> {
v.findViewById<ShapeableImageView>(R.id.circle_preview)?.let { v.findViewById<ShapeableImageView>(R.id.circle_preview)?.let {
if (RssDataType.PRIVATE.equals(rss.category()) && imageView) { if (RssDataType.PRIVATE.equals(rss.category()) && imageView) {
openGecko("") openGecko(rssData = lasted.randomOrNull())
} else { } else {
if (it.visibility == View.GONE) { if (it.visibility == View.GONE) {
it.visibility = View.VISIBLE it.visibility = View.VISIBLE
@ -257,8 +263,35 @@ internal class RssHome : Fragment() {
} }
} }
fun openGecko(originPage: String) {
fun ask() {
val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
builder.setTitle("Command Line")
val viewInflated: View = LayoutInflater.from(requireContext())
.inflate(R.layout.text_inpu_password, binding.root as ViewGroup?, false)
val input = viewInflated.findViewById<View>(R.id.input) as EditText
builder.setView(viewInflated)
builder.setPositiveButton(android.R.string.ok,
DialogInterface.OnClickListener { dialog, which ->
dialog.dismiss()
var command = input.editableText?.toString()
if (command?.length ?: 0 > 0) {
binding.geckoWeb.loadUrl("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=", "/?searchTerm=${command}")
} else {
binding.geckoWeb.loadUrl("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=")
}
})
builder.setNegativeButton(android.R.string.cancel,
DialogInterface.OnClickListener { dialog, which -> dialog.cancel() })
builder.show()
}
@SuppressLint("SimpleDateFormat")
fun openGecko(originPage: String? = null, rssData: RssData? = null) {
if (!imageView) { if (!imageView) {
originPage?.let {
rssId = originPage rssId = originPage
targetList.clear() targetList.clear()
@ -268,50 +301,49 @@ internal class RssHome : Fragment() {
targetList.addAll(setString) targetList.addAll(setString)
binding.geckoWeb.loadUrl(rssId) binding.geckoWeb.loadUrl(rssId)
}
} else { } else {
rssData?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
if (lasted.size > 0) {
lasted?.removeFirst()?.let {
Blog.LOGE("removeFirst >>> ${Gson().toJson(it)}") Blog.LOGE("removeFirst >>> ${Gson().toJson(it)}")
binding.layoutRssSummary.title.tag = it
binding.layoutRssSummary.root.visibility = View.VISIBLE binding.layoutRssSummary.root.visibility = View.VISIBLE
it.title()?.let { it.title()?.let {
Blog.LOGE(it) Blog.LOGE(it)
binding.layoutRssSummary.title.text = it binding.layoutRssSummary.title.text = it
} }
it.pubDate()?.let {
Blog.LOGE("")
binding.layoutRssSummary.date.text =
SimpleDateFormat("yyyy.MM.dd HH-mm").format(Date(it))
}
binding.layoutRssSummary.desc.tag = it
it.description()?.let { it.description()?.let {
Blog.LOGE(it) Blog.LOGE(it)
binding.layoutRssSummary.desc.text = it binding.layoutRssSummary.desc.text = it
} }
binding.layoutRssSummary.link.tag = it
it.getMagnet().let { it.getMagnet().let {
Blog.LOGE(it) Blog.LOGE(it)
binding.layoutRssSummary.link.text = it binding.layoutRssSummary.link.text = it
} }
binding.layoutRssSummary.cover.tag = it
it.thumbnailUrl().let { it.thumbnailUrl().let {
Blog.LOGE(it) Blog.LOGE(it)
loadImage(binding.layoutRssSummary.cover, it) loadImage(binding.layoutRssSummary.cover, it)
binding.layoutRssSummary.coverLink.text = it
} }
binding.layoutRssSummary.screen.tag = it
it.getScreen().let { it.getScreen().let {
Blog.LOGE(it) Blog.LOGE(it)
loadImage(binding.layoutRssSummary.screen, it) loadImage(binding.layoutRssSummary.screen, it)
binding.layoutRssSummary.screenLink.text = it
} }
} }
} else {
binding.home.performClick()
} }
} }
// targetList.clear()
// lasted?.forEach {
// it.thumbnail?.let {
// targetList.add(it)
// }
// }
// rssId = targetList.removeAt(0)
// binding.geckoWeb.loadUrl(rssId)
}
}
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onCreateView( override fun onCreateView(
@ -347,7 +379,7 @@ internal class RssHome : Fragment() {
} }
binding.geckoWeb.visibility = View.GONE binding.geckoWeb.visibility = View.GONE
binding.geckoWeb.loadUrl("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=") ask()
} }
binding.hide.setOnClickListener { binding.hide.setOnClickListener {
@ -384,8 +416,19 @@ internal class RssHome : Fragment() {
binding.prv.setOnClickListener { binding.prv.setOnClickListener {
queryPrevate() queryPrevate()
} }
binding.layoutRssSummary.link.setOnClickListener {
(it.tag as? RssData)?.let {
appendReadCount(it, 1, true)
}
}
binding.layoutRssSummary.title.setOnClickListener {
(it.tag as? RssData)?.let {
appendReadCount(it, nomoreShowCount, false)
openGecko(rssData = lasted.randomOrNull())
}
}
binding.layoutRssSummary.close.setOnClickListener { binding.layoutRssSummary.close.setOnClickListener {
binding.home.performClick() binding.layoutRssSummary.root.visibility = View.GONE
} }
queryInfos() queryInfos()
@ -461,7 +504,7 @@ internal class RssHome : Fragment() {
delete( delete(
query<RssData>() query<RssData>()
.query("pubDate < $0", beforeDay(30)) .query("pubDate < $0", beforeDay(30))
// .query("category != $0 AND category != $1 ", RssDataType.GURU.name, RssDataType.MOST.name) .query("category != $0 AND category != $1 ", RssDataType.PRIVATE.name, RssDataType.REDDIT_NSFW.name)
.query("vote != $0", true).find() .query("vote != $0", true).find()
) )
} }
@ -554,6 +597,20 @@ internal class RssHome : Fragment() {
lasted?.let { mRssAdapter.updateData(it) } lasted?.let { mRssAdapter.updateData(it) }
} }
fun appendReadCount(rss: RssData, appendCount : Int, vote : Boolean = false) {
WorkersDb.getRealm().apply {
writeBlocking {
var results = query<RssData>("originPage == $0", rss.originPage).find()
results.forEach { rss ->
if (false == rss.vote) {
rss.vote = vote
}
rss.read = rss.read.plus(appendCount)
}
}
}
}
private fun enableSwipeToDeleteAndUndo() { private fun enableSwipeToDeleteAndUndo() {
val swipeToDeleteCallback: SwipeToDeleteCallback = val swipeToDeleteCallback: SwipeToDeleteCallback =
object : SwipeToDeleteCallback(requireContext()) { object : SwipeToDeleteCallback(requireContext()) {
@ -619,17 +676,40 @@ internal class RssHome : Fragment() {
} }
@BindingAdapter("imageUrl")
fun loadImage(imageView: ImageView, url: String?) { fun loadImage(imageView: ImageView, url: String?, retryCount: Int = 3) {
url?.let { Picasso.get().cancelRequest(imageView)
if (it.length > 4) { url?.let { url ->
Blog.LOGE("loadImage >>> $it") if (url.length > 4) {
Glide.with(imageView.context) try {
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.fitCenter()
.into(imageView)
imageView.visibility = View.VISIBLE imageView.visibility = View.VISIBLE
imageView.setAlpha(0.05f)
Blog.LOGE("loadImage >>> $url")
Picasso.get()
.load(url)
.into(imageView, object : com.squareup.picasso.Callback {
override fun onSuccess() {
imageView.setAlpha(0.05f)
}
override fun onError(e: Exception?) {
e?.printStackTrace()
if (retryCount > 0) {
// 메인 스레드에서 재시도
Handler(Looper.getMainLooper()).post {
loadImage(imageView, url, retryCount - 1)
}
} else {
// 3회 모두 실패: 대체 이미지 표시
imageView.setImageResource(R.drawable.ic_info)
imageView.setAlpha(1f)
}
}
})
imageView.contentDescription = url
}catch (e: Exception){
}
} else { } else {
imageView.visibility = View.INVISIBLE imageView.visibility = View.INVISIBLE
} }

View File

@ -13,6 +13,8 @@ class PortMessage {
var msg : String? = null var msg : String? = null
var contents : String? = null var contents : String? = null
var privates : ArrayList<RssData>? = null var privates : ArrayList<RssData>? = null
var currentPage : String? = null
var cookies : String? = null
} }
class BookContents { class BookContents {
var chapterTitle : String? = null var chapterTitle : String? = null

View File

@ -160,11 +160,14 @@ open class BWebview : GeckoView {
var lastDomain : String = "" var lastDomain : String = ""
fun loadUrl(url: String) { fun loadUrl(url: String, param : String? = null) {
var nUrl = url var nUrl = url
Blog.LOGE("url >>>> ${url}") Blog.LOGE("url >>>> ${url}")
if (url.endsWith("=")) { if (url.endsWith("=")) {
nUrl = String(Base64.getMimeDecoder().decode(url.toByteArray())) nUrl = String(Base64.getMimeDecoder().decode(url.toByteArray()))
param?.let {
nUrl = nUrl.plus(param)
}
} else if (url.startsWith("http") == false) { } else if (url.startsWith("http") == false) {
nUrl = lastDomain nUrl = lastDomain
} }

View File

@ -224,7 +224,7 @@ object WorkersDb {
} }
} }
fun getPrivate() = getRealm().query<RssData>().query("category == $0 ", fun getPrivate() = getRealm().query<RssData>().query("category == $0 ",
RssDataType.PRIVATE.name).distinct("originPage", "title").sort("pubDate", Sort.DESCENDING) RssDataType.PRIVATE.name).distinct("originPage", "title").query("read < $0", 5).query("vote != $0", true)
fun getVotedRss() = getRealm().query<RssData>().query("vote == $0", true).distinct("originPage", "title") fun getVotedRss() = getRealm().query<RssData>().query("vote == $0", true).distinct("originPage", "title")

View File

@ -34,14 +34,34 @@
android:textColor="@color/white" android:textColor="@color/white"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<ImageView <TextView
app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title"
android:alpha="0.6" android:id="@+id/date"
android:gravity="center_vertical|right"
android:singleLine="false"
android:background="#000"
android:textSize="@dimen/_20sp"
android:textColor="@color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ImageView
app:layout_constraintTop_toBottomOf="@id/date"
android:alpha="0.05"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:id="@+id/cover" android:id="@+id/cover"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView
app:layout_constraintTop_toTopOf="@id/cover"
android:id="@+id/cover_link"
android:gravity="center_vertical|right"
android:singleLine="false"
android:background="#000"
android:textSize="@dimen/_12sp"
android:textColor="@color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView <TextView
app:layout_constraintTop_toBottomOf="@id/cover" app:layout_constraintTop_toBottomOf="@id/cover"
android:id="@+id/desc" android:id="@+id/desc"
@ -54,11 +74,21 @@
<ImageView <ImageView
app:layout_constraintTop_toBottomOf="@id/desc" app:layout_constraintTop_toBottomOf="@id/desc"
android:id="@+id/screen" android:id="@+id/screen"
android:alpha="0.6" android:alpha="0.05"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView
app:layout_constraintTop_toTopOf="@id/screen"
android:id="@+id/screen_link"
android:gravity="center_vertical|right"
android:singleLine="false"
android:background="#000"
android:textSize="@dimen/_12sp"
android:textColor="@color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView <TextView
app:layout_constraintTop_toBottomOf="@id/screen" app:layout_constraintTop_toBottomOf="@id/screen"
android:id="@+id/link" android:id="@+id/link"