This commit is contained in:
lunaticbum 2024-12-24 18:12:54 +09:00
parent 2be0549b73
commit d00c97e7d7
8 changed files with 334 additions and 295 deletions

View File

@ -0,0 +1,250 @@
package bums.lunatic.launcher.home
import android.content.Context
import android.content.Intent
import android.os.Environment
import android.webkit.CookieManager
import androidx.core.content.FileProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kr.lunaticbum.awesomewebview.helpers.DownPicUtil
import kr.lunaticbum.awesomewebview.helpers.DownPicUtil.DownFinishListener
import kr.lunaticbum.utils.log.LogUtil
import org.jsoup.Jsoup
import org.jsoup.UnsupportedMimeTypeException
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Date
val defaultTime = 200L
class OfflineContents(val context: Context, val host : String, val cookie: String, val agent: String, val current: String, val newPath: String, val value: String, val autoCheck : Boolean, val mediaUrls : ArrayList<String>) {
val targetFile : File
val filePath = "index.html"
val path = File(
Environment.getExternalStorageDirectory(),
"bums"
)
val newFolder = File(path, newPath).apply { mkdirs() }
val newFile : File
var htmlString : StringBuffer
val urlPathMap : HashMap<String,String> = hashMapOf()
var reqCount = 0
var endOfLooPCheck = false
val lDownFinishListener = object : DownFinishListener {
override fun onDownFinish(url: String, path: String) {
LogUtil.e("Url >> ${url} path >> ${path}")
urlPathMap.put(url, path)
reqCount -= 1
if (reqCount == 0 && endOfLooPCheck) {
enofLoop()
}
}
override fun onError() {
reqCount -= 1
if (reqCount == 0 && endOfLooPCheck) {
enofLoop()
}
}
}
init {
targetFile = File(newFolder, filePath)
newFile = File(
path,
newPath.plus(SimpleDateFormat("yyyMMdd").format(Date())).plus(".html")
)
htmlString= trimHtnl(value, host)
}
var onItEndof = false
fun enofLoop() {
if (onItEndof) return
onItEndof = true
LogUtil.e("on it enofLoop")
urlPathMap.forEach { t, u ->
val file = File(u)
var contentsUriString = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
).toString()
var targetString = t
var targetIdx = htmlString.indexOf(targetString)
if (targetIdx > 0) {
htmlString?.replace(
targetIdx,
targetIdx.plus(targetString.length),
contentsUriString
)
}
targetString = t.replace("&", "&amp;")
targetIdx = htmlString.indexOf(targetString)
if (targetIdx > 0) {
htmlString?.replace(
targetIdx,
targetIdx.plus(targetString.length),
contentsUriString
)
}
}
if (autoCheck) {
LogUtil.e("on it enofLoop autoCheck ${autoCheck}")
moveFile()
context.startActivity(Intent(context,RssViewerActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
newFile
)
})
} else {
moveFile()
}
}
fun moveFile() {
indexSave(htmlString, targetFile, host)
targetFile.copyTo(newFile)
if (targetFile.parentFile.isDirectory) {
targetFile.parentFile.listFiles().forEach {
it.delete()
}
targetFile.parentFile.delete()
}
targetFile.delete()
}
fun excute() {
CoroutineScope(Dispatchers.IO).launch {
LogUtil.e("onHtml value >> ${value}")
mediaUrls.forEach { url ->
var downPic = false
try {
LogUtil.e("try Jsoup.parse ${url}")
Jsoup.parse(URL(url), defaultTime.times(4).toInt()).let {
try {
LogUtil.e("onit Jsoup.parse ${it.title()}")
if (it.select("svg").size > 0) {
try {
downPic(url, agent, current!!, cookie!!, lDownFinishListener)
reqCount += 1
} catch (e : Exception) {
e.printStackTrace()
}
} else {
it.getElementsByTag("video")?.forEach {
it.attribute("src").value?.let {
var url = if (it.startsWith("http")) {
it
} else {
"https:".plus(it)
}
try {
downMp4(url, agent, current!!, cookie!!, lDownFinishListener)
reqCount += 1
} catch (e : Exception) {
e.printStackTrace()
}
}
}
}
} catch (e: UnsupportedMimeTypeException) {
LogUtil.e("e.message3 ${e.message}")
} catch (e: Exception) {
LogUtil.e("e.message4 ${e.message}")
}
}
} catch (e: UnsupportedMimeTypeException) {
LogUtil.e("e.message ${e.message}")
LogUtil.e("e.message ${e.localizedMessage}")
if (e.message?.contains("Must be text") == true) {
downPic = true
}
} catch (e: Exception) {
if (url.contains("dcimg") == true) {
downPic = true
}
LogUtil.e("e.message2 ${e.message}")
} finally {
if (downPic) {
try {
downPic(url, agent, current!!, cookie!!, lDownFinishListener)
reqCount += 1
} catch (e : Exception) {
e.printStackTrace()
}
}
}
delay(defaultTime)
}
LogUtil.e("END OF LOOP ${reqCount}")
endOfLooPCheck = true
delay(defaultTime.times(4))
if (reqCount <= 0) {
enofLoop()
}
}
}
fun indexSave(htmlString:StringBuffer, targetFile :File, host: String) {
BufferedWriter(FileWriter(targetFile)).use { writer ->
trimHtnl(htmlString.toString(), host)?.let { result ->
writer.write(result.toString())
}
}
}
fun trimHtnl(target : String, host : String) : StringBuffer {
var result = target.replace("\\\"","\"").replace("\\n\\t\\t\\t","").replace("\\n\\t\\t","").replace("\\n\\t","").replace("\\t", "").replace("\\n", "").replace("\"\"\"","").replace("href=\"//","href=\"https://").replace("src=\"//","src=\"https://").replace("url(\"//","url(\"https://").replace("url(&quot;//","url(&quot;https://")
if (host?.contains("clien") == true) {
result = result.replace("href=\"/service","href=\"https://m.clien.net/service").replace("href=\"\\&quot;/service","href=\"\\&quot;https://m.clien.net/service")
}
return StringBuffer(result)
}
fun downMp4(url : String, agent : String, current : String,cookie : String, listner :DownFinishListener) {
LogUtil.e("try imageFile down ${url}")
val path = File(
Environment.getExternalStorageDirectory(),
"bums"
)
DownPicUtil.downMp4(
File(
path,"private_mp4"
).path,
url,
agent,
current,
cookie,
listner
)
}
fun downPic( url : String, agent : String, current : String,cookie : String, listner :DownFinishListener) {
LogUtil.e("try imageFile down ${url}")
val cookieManager =
CookieManager.getInstance()
val cookie =
cookieManager.getCookie(current)
val path = File(
Environment.getExternalStorageDirectory(),
"bums"
)
DownPicUtil.downPic(
File(path, "private_img").path,
url,
agent,
current,
cookie,
listner)
}
}

View File

@ -33,6 +33,7 @@ import bums.lunatic.launcher.workers.WorkersDb
import io.realm.kotlin.ext.query
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kr.lunaticbum.awesomewebview.AwesomeWebView
import kr.lunaticbum.awesomewebview.AwesomeWebViewActivity
@ -55,6 +56,8 @@ import java.net.URL
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Date
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.text.Charsets.UTF_8
@ -264,37 +267,25 @@ class RssViewerActivity : AwesomeWebViewActivity(), View.OnGenericMotionListene
}
fun rightClick() {
if (currentIdx < rssList.size - 1) {
currentIdx += 1
rssId = rssList.removeAt(currentIdx)
Blog.LOGE("Arrow Right Click ${currentIdx} ${rssId}")
load(rssId)
registCancelSearch()
} else {
Toast.makeText(this, "없어 끄자", Toast.LENGTH_LONG).show()
fast()
}
doNextPage()
}
fun leftClick() {
if (currentIdx > 0) {
currentIdx -= 1
rssId = rssList.removeAt(currentIdx)
Blog.LOGE("Arrow Left Click ${currentIdx} ${rssId}")
load(rssId)
registCancelSearch()
} else {
Toast.makeText(this, "없어 끄자", Toast.LENGTH_LONG).show()
fast()
}
Blog.LOGE("Arrow Left Click")
hideRss()
}
fun getUnit() = ((webView?.height ?: 0) * 0.4).toInt()
fun scrollDown() {
Blog.LOGE("Arrow Down Click")
registCancelSearch()
webView?.scrollTo(webView?.scrollX ?: 0, (webView?.scrollY ?: 0) + getUnit())
try {
Blog.LOGE("Arrow Down Click")
registCancelSearch()
runOnUiThread {
webView?.scrollTo(webView?.scrollX?: 0,(webView?.scrollY?: 0) + getUnit())
}
} catch (e : Exception) {
e.printStackTrace()
}
}
override var pdfListner : PDFPrint.OnPDFPrintListener? = object : PDFPrint.OnPDFPrintListener {
@ -316,7 +307,7 @@ class RssViewerActivity : AwesomeWebViewActivity(), View.OnGenericMotionListene
}
}
// val tika: Tika = Tika()
// val tika: Tika = Tika()
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
if (ev?.device?.name?.contains("BLE-M3") == true) {
Blog.LOGE("keyEvent >>>>> dispatchGenericMotionEvent ${ev}")
@ -451,6 +442,9 @@ class RssViewerActivity : AwesomeWebViewActivity(), View.OnGenericMotionListene
return Base64.encodeToString(bytes, Base64.DEFAULT)
}
fun run(value : String, autoCheck : Boolean) {
webView!!.setOnScrollChangeListener(null)
var mediaUrls = arrayListOf<String>()
@ -465,225 +459,34 @@ class RssViewerActivity : AwesomeWebViewActivity(), View.OnGenericMotionListene
val agent = webView!!.settings.userAgentString
val current = webView!!.url
val newPath = hashString(current!!, "MD5").toHex()
CoroutineScope(Dispatchers.IO).launch {
LogUtil.e("onHtml value >> ${value}")
val path = File(
Environment.getExternalStorageDirectory(),
"bums"
)
val filePath = "index.html"
val newFolder = File(path, newPath).apply { mkdirs() }
var htmlString = trimHtnl(value!!)
val targetFile = File(newFolder, filePath)
var reqCount = 0
var endOfLooPCheck = false
var urlPathMap = hashMapOf<String, String>()
var newFile = File(
path,
newPath.plus(SimpleDateFormat("yyyMMdd").format(Date())).plus(".html")
)
fun moveFile() {
indexSave(htmlString, targetFile)
targetFile.copyTo(newFile)
if (targetFile.parentFile.isDirectory) {
targetFile.parentFile.listFiles().forEach {
it.delete()
}
targetFile.parentFile.delete()
}
targetFile.delete()
}
fun enofLoop() {
CoroutineScope(Dispatchers.IO).launch {
LogUtil.e("on it enofLoop")
urlPathMap.forEach { t, u ->
val file = File(u)
var contentsUriString = FileProvider.getUriForFile(
this@RssViewerActivity,
"${this@RssViewerActivity.packageName}.fileprovider",
file
).toString()
// try {
// val detectedType: String = tika.detect(file)
// e("Detected type: $detectedType")
// } catch (e: IOException) {
// e("Error detecting type: " + e.message)
// }
var targetString = t
var targetIdx = htmlString.indexOf(targetString)
if (targetIdx > 0) {
htmlString?.replace(
targetIdx,
targetIdx.plus(targetString.length),
contentsUriString
)
}
targetString = t.replace("&", "&amp;")
targetIdx = htmlString.indexOf(targetString)
if (targetIdx > 0) {
htmlString?.replace(
targetIdx,
targetIdx.plus(targetString.length),
contentsUriString
)
}
}
if (autoCheck) {
LogUtil.e("on it enofLoop autoCheck ${autoCheck}")
webView?.postDelayed({
hideBlock()
webView?.setOnScrollChangeListener(null)
moveFile()
startActivity(Intent(this@RssViewerActivity,RssViewerActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = FileProvider.getUriForFile(
this@RssViewerActivity,
"${this@RssViewerActivity.packageName}.fileprovider",
newFile
)
})
}, defaultTime.times(4))
} else {
runOnUiThread {
moveFile()
Toast.makeText(
applicationContext,
resources.getString(
stringResPhotoSavedTo
) + targetFile.absolutePath,
Toast.LENGTH_LONG
).show()
hideBlock()
doNextPage()
LogUtil.e("on it enofLoop autoCheck ${autoCheck}")
}
}
}
}
val lDownFinishListener = object : DownFinishListener {
override fun onDownFinish(url: String, path: String) {
LogUtil.e("Url >> ${url} path >> ${path}")
urlPathMap.put(url, path)
reqCount -= 1
if (reqCount == 0 && endOfLooPCheck) {
enofLoop()
}
}
override fun onError() {
if (showToastPhotoSavedOrFailed) {
Toast.makeText(
applicationContext,
resources.getString(
stringResPhotoSaveFailed
),
Toast.LENGTH_LONG
).show()
}
reqCount -= 1
if (reqCount == 0 && endOfLooPCheck) {
enofLoop()
}
}
}
mediaUrls.forEach { url ->
var downPic = false
try {
LogUtil.e("try Jsoup.parse ${url}")
Jsoup.parse(URL(url), defaultTime.times(4).toInt()).let {
try {
LogUtil.e("onit Jsoup.parse ${it.title()}")
if (it.select("svg").size > 0) {
try {
downPic(url, agent, current!!, cookie!!, lDownFinishListener)
reqCount += 1
} catch (e : Exception) {
e.printStackTrace()
}
} else {
it.getElementsByTag("video")?.forEach {
it.attribute("src").value?.let {
var url = if (it.startsWith("http")) {
it
} else {
"https:".plus(it)
}
try {
downMp4(url, agent, current!!, cookie!!, lDownFinishListener)
reqCount += 1
} catch (e : Exception) {
e.printStackTrace()
}
}
}
}
} catch (e: UnsupportedMimeTypeException) {
LogUtil.e("e.message3 ${e.message}")
} catch (e: Exception) {
LogUtil.e("e.message4 ${e.message}")
}
}
} catch (e: UnsupportedMimeTypeException) {
LogUtil.e("e.message ${e.message}")
LogUtil.e("e.message ${e.localizedMessage}")
if (e.message?.contains("Must be text") == true) {
downPic = true
}
} catch (e: Exception) {
if (url.contains("dcimg") == true) {
downPic = true
}
LogUtil.e("e.message2 ${e.message}")
} finally {
if (downPic) {
try {
downPic(url, agent, current!!, cookie!!, lDownFinishListener)
reqCount += 1
} catch (e : Exception) {
e.printStackTrace()
}
}
}
sleep(defaultTime)
}
LogUtil.e("END OF LOOP ${reqCount}")
endOfLooPCheck = true
if (reqCount <= 0) {
webView?.postDelayed({
enofLoop()
},defaultTime.times(4))
Executors.newSingleThreadScheduledExecutor().schedule({
OfflineContents(this@RssViewerActivity, host!!,cookie,agent,current,newPath,value,autoCheck,mediaUrls).excute()
}, defaultTime, TimeUnit.MILLISECONDS)
runOnUiThread {
hideBlock()
if (!autoCheck) {
doNextPage()
}
}
}
val defaultTime = 200L
override fun onHtml(value: String?, autoCheck : Boolean) {
chechHandler.removeCallbacks(cancelSearch)
if (loadWithIntent){
return
}
showBlock()
var count = (webView!!.contentHeight / (webView!!.height * 0.3).toInt())
LogUtil.e("count >>>> ${count} webView!!.contentHeight >>>> ${webView!!.contentHeight} , webView!!.height >>>> ${webView!!.height} :: ${(webView!!.height * 0.3).toInt()}")
webView!!.postDelayed({
webView!!.pageDown(false)
},defaultTime)
chechHandler.postDelayed(pageDown, defaultTime)
webView!!.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
val measuredHeight: Int = v.measuredHeight
LogUtil.e("OnScrollChange >>> ${scrollY} , ${oldScrollY}")
if(measuredHeight + scrollY == webView!!.computeVerticalScrollRange()){
webView!!.postDelayed({
chechHandler.removeCallbacks(scrollDown)
chechHandler.removeCallbacks(pageDown)
chechHandler.postDelayed({
webView!!.evaluateJavascript("document.documentElement.outerHTML",object : ValueCallback<String> {
override fun onReceiveValue(value: String?) {
val html = value?.replace("\\u003C", "<")
@ -692,66 +495,39 @@ class RssViewerActivity : AwesomeWebViewActivity(), View.OnGenericMotionListene
})
},defaultTime.times(2))
} else {
webView!!.postDelayed({
webView!!.pageDown(false)
},defaultTime)
chechHandler.removeCallbacks(cancelSearch)
Blog.LOGE("onScrollChanged called PageDown")
// chechHandler.removeCallbacks(scrollDown)
if (switch) {
chechHandler.removeCallbacks(pageDown)
chechHandler.postDelayed(scrollDown, defaultTime)
switch = false
} else {
switch = true
chechHandler.removeCallbacks(scrollDown)
chechHandler.postDelayed(pageDown, defaultTime)
}
}
// else if (scrollY % 10 == 0 || oldScrollY % 10 == 0){
//// chechHandler.removeCallbacks(pageDown)
//// chechHandler.postDelayed(scrollDown, defaultTime)
// }
}
}
var switch = false
var scrollDown : Runnable = Runnable{
webView?.let {
var times = if (it.scrollY > 50) it.scrollY / 50 else 5
it.scrollTo(it.scrollX,times.plus(1).times(50))
}
}
fun trimHtnl(target : String) : StringBuffer {
var result = target.replace("\\\"","\"").replace("\\n\\t\\t\\t","").replace("\\n\\t\\t","").replace("\\n\\t","").replace("\\t", "").replace("\\n", "").replace("\"\"\"","").replace("href=\"//","href=\"https://").replace("src=\"//","src=\"https://").replace("url(\"//","url(\"https://").replace("url(&quot;//","url(&quot;https://")
if (host?.contains("clien") == true) {
result = result.replace("href=\"/service","href=\"https://m.clien.net/service").replace("href=\"\\&quot;/service","href=\"\\&quot;https://m.clien.net/service")
}
return StringBuffer(result)
var pageDown : Runnable = Runnable{
webView!!.pageDown(false)
}
fun indexSave(htmlString:StringBuffer, targetFile :File) {
BufferedWriter(FileWriter(targetFile)).use { writer ->
trimHtnl(htmlString.toString())?.let { result ->
writer.write(result.toString())
}
}
}
fun downMp4(url : String, agent : String, current : String,cookie : String, listner :DownFinishListener) {
LogUtil.e("try imageFile down ${url}")
val path = File(
Environment.getExternalStorageDirectory(),
"bums"
)
DownPicUtil.downMp4(
File(
path,"private_mp4"
).path,
url,
agent,
current,
cookie,
listner
)
}
fun downPic( url : String, agent : String, current : String,cookie : String, listner :DownFinishListener) {
LogUtil.e("try imageFile down ${url}")
val cookieManager =
CookieManager.getInstance()
val cookie =
cookieManager.getCookie(current)
val path = File(
Environment.getExternalStorageDirectory(),
"bums"
)
DownPicUtil.downPic(
File(path, "private_img").path,
url,
agent,
current,
cookie,
listner)
}
override fun webviewOnPageFinished() {
double = false
if(hasYoutubePlayer) {

View File

@ -38,6 +38,7 @@ dependencies {
implementation( "com.github.bumptech.glide:glide:4.11.0")
annotationProcessor ("com.github.bumptech.glide:compiler:4.11.0")
// implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation ("com.airbnb.android:lottie:5.2.0")
implementation ("androidx.annotation:annotation:1.9.1")
implementation ("androidx.appcompat:appcompat:1.7.0")
implementation ("com.google.android.material:material:1.12.0")

View File

@ -95,6 +95,7 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.abs
import kotlin.random.Random
open class AwesomeWebViewActivity : AppCompatActivity(), View.OnClickListener,
@ -1252,10 +1253,14 @@ open class AwesomeWebViewActivity : AppCompatActivity(), View.OnClickListener,
protected fun showBlock() {
binding.blocking.visibility = View.VISIBLE
val ress = arrayListOf(R.raw.lt_lodaing_01,R.raw.lt_lodaing_02,R.raw.lt_lodaing_03)
binding.lotti.setAnimation(ress[Math.abs(Random(8736).nextInt()) % ress.size])
binding.lotti.playAnimation()
}
protected fun hideBlock() {
binding.blocking.visibility = View.GONE
binding.lotti.pauseAnimation()
}
protected fun buildWebView(): VideoEnabledWebView {
@ -1921,7 +1926,7 @@ open class AwesomeWebViewActivity : AppCompatActivity(), View.OnClickListener,
.contains("ads".toLowerCase(Locale.ROOT))) {
LogUtil.e("shouldInterceptRequest request url contains ads >>> ${request.url.toString()}")
}
var adblockKeyWords = arrayOf("adcr.naver.com","daumcdn.net/biz/ui/ad/adcm","imgad","ad.daum.net","cr.adsappier.com","ar-adview","adtrafficquality","criteo","adlib.nhnace.com","google.com/ads","googleads.","/pagead","/adpost/","ads/search","plugin.adplex")
var adblockKeyWords = arrayOf("adcr.naver.com","daumcdn.net/biz/ui/ad/adcm","imgad","ad.daum.net","cr.adsappier.com","ar-adview","adtrafficquality","criteo","adlib.nhnace.com","google.com/ads","googleads.","/pagead","/adpost/","ads/search","plugin.adplex","google-analytics.com")
val adblock = adblockKeyWords.filter { url.toLowerCase(Locale.ROOT).contains(it.toLowerCase(Locale.ROOT)) }.size > 0
return if(adblock) {
try {
@ -1975,6 +1980,7 @@ open class AwesomeWebViewActivity : AppCompatActivity(), View.OnClickListener,
// }
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
mediaUrls.clear()
BroadCastManager.onPageStarted(this@AwesomeWebViewActivity, key, url)
if (!url.contains("docs.google.com") && url.endsWith(".pdf")) {

View File

@ -85,28 +85,31 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/blocking"
android:visibility="gone"
android:background="#EE000000"
android:background="#99000000"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:visibility="visible"
app:trackThickness="10dp"
android:layout_margin="20dp"
app:trackColor="@color/finestWhite"
app:indicatorColor="@color/Color_FireBrick"
android:layout_gravity="center_horizontal"
android:progress="80"
app:trackCornerRadius="8dp"
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lotti"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_rawRes="@raw/lt_lodaing_01"
app:lottie_repeatMode="reverse"
/>
<TextView
app:layout_constraintTop_toBottomOf="@id/lotti"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="30dp"
android:text="저장을 위해\n리소스 모으는중..."
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long