Merge branch 'master' of https://github.com/recloudstream/cloudstream into recloudstream-master

# Conflicts:
#	app/src/main/ic_launcher-playstore.png
#	app/src/main/res/mipmap-hdpi/ic_launcher.png
#	app/src/main/res/mipmap-hdpi/ic_launcher_round.png
#	app/src/main/res/mipmap-mdpi/ic_launcher.png
#	app/src/main/res/mipmap-mdpi/ic_launcher_round.png
#	app/src/main/res/mipmap-xhdpi/ic_launcher.png
#	app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
#	app/src/main/res/mipmap-xxhdpi/ic_launcher.png
#	app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
#	app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
#	app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
#	app/src/prerelease/ic_launcher-playstore.png
#	app/src/prerelease/res/mipmap-hdpi/ic_launcher.png
#	app/src/prerelease/res/mipmap-hdpi/ic_launcher_round.png
#	app/src/prerelease/res/mipmap-mdpi/ic_launcher.png
#	app/src/prerelease/res/mipmap-mdpi/ic_launcher_round.png
#	app/src/prerelease/res/mipmap-xhdpi/ic_launcher.png
#	app/src/prerelease/res/mipmap-xhdpi/ic_launcher_round.png
#	app/src/prerelease/res/mipmap-xxhdpi/ic_launcher.png
#	app/src/prerelease/res/mipmap-xxhdpi/ic_launcher_round.png
#	app/src/prerelease/res/mipmap-xxxhdpi/ic_launcher.png
#	app/src/prerelease/res/mipmap-xxxhdpi/ic_launcher_round.png
This commit is contained in:
KillerDogeEmpire 2022-09-22 15:18:58 -07:00
commit c49c34ee92
31 changed files with 167 additions and 19 deletions

BIN
.github/downloads.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 58 KiB

BIN
.github/home.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 136 KiB

BIN
.github/player.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 48 KiB

BIN
.github/results.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 96 KiB

BIN
.github/search.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -150,7 +150,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
* @return true if the str has launched an app task (be it successful or not) * @return true if the str has launched an app task (be it successful or not)
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login. * @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
* */ * */
fun handleAppIntentUrl(activity: FragmentActivity?, str: String?, isWebview: Boolean): Boolean = fun handleAppIntentUrl(
activity: FragmentActivity?,
str: String?,
isWebview: Boolean
): Boolean =
with(activity) { with(activity) {
if (str != null && this != null) { if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) { if (str.startsWith("https://cs.repo")) {
@ -191,7 +195,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val url = str.replaceFirst(appStringRepo, "https") val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url) loadRepository(url)
return true return true
} else if (!isWebview){ } else if (!isWebview) {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) { if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads) this.navigate(R.id.navigation_downloads)
return true return true
@ -565,9 +569,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
for (api in accountManagers) { for (api in accountManagers) {
api.init() api.init()
} }
}
ioSafe {
inAppAuths.apmap { api -> inAppAuths.apmap { api ->
try { try {
api.initialize() api.initialize()

View File

@ -1,13 +1,19 @@
package com.lagradost.cloudstream3.plugins package com.lagradost.cloudstream3.plugins
import android.app.*
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import com.google.gson.Gson import com.google.gson.Gson
import android.content.res.AssetManager import android.content.res.AssetManager
import android.content.res.Resources import android.content.res.Resources
import android.os.Environment import android.os.Environment
import android.widget.Toast import android.widget.Toast
import android.app.Activity import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
@ -25,7 +31,9 @@ import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.extractorApis
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -38,6 +46,9 @@ import java.util.*
const val PLUGINS_KEY = "PLUGINS_KEY" const val PLUGINS_KEY = "PLUGINS_KEY"
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL" const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
const val EXTENSIONS_CHANNEL_ID = "cloudstream3.extensions"
const val EXTENSIONS_CHANNEL_NAME = "Extensions"
const val EXTENSIONS_CHANNEL_DESCRIPT = "Extension notification channel"
// Data class for internal storage // Data class for internal storage
data class PluginData( data class PluginData(
@ -78,6 +89,8 @@ object PluginManager {
const val TAG = "PluginManager" const val TAG = "PluginManager"
private var hasCreatedNotChanel = false
/** /**
* Store data about the plugin for fetching later * Store data about the plugin for fetching later
* */ * */
@ -220,8 +233,11 @@ object PluginManager {
"Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}" "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}"
} }
val updatedPlugins = mutableListOf<String>()
outdatedPlugins.apmap { pluginData -> outdatedPlugins.apmap { pluginData ->
if (pluginData.isDisabled) { if (pluginData.isDisabled) {
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
unloadPlugin(pluginData.savedData.filePath) unloadPlugin(pluginData.savedData.filePath)
} else if (pluginData.isOutdated) { } else if (pluginData.isOutdated) {
downloadAndLoadPlugin( downloadAndLoadPlugin(
@ -229,10 +245,17 @@ object PluginManager {
pluginData.onlineData.second.url, pluginData.onlineData.second.url,
pluginData.savedData.internalName, pluginData.savedData.internalName,
pluginData.onlineData.first pluginData.onlineData.first
) ).let { success ->
if (success)
updatedPlugins.add(pluginData.onlineData.second.name)
}
} }
} }
main {
createNotification(activity, updatedPlugins)
}
ioSafe { ioSafe {
afterPluginsLoadedEvent.invoke(true) afterPluginsLoadedEvent.invoke(true)
} }
@ -438,4 +461,59 @@ object PluginManager {
false false
} }
} }
private fun Context.createNotificationChannel() {
hasCreatedNotChanel = true
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = EXTENSIONS_CHANNEL_NAME //getString(R.string.channel_name)
val descriptionText = EXTENSIONS_CHANNEL_DESCRIPT//getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_LOW
val channel = NotificationChannel(EXTENSIONS_CHANNEL_ID, name, importance).apply {
description = descriptionText
}
// Register the channel with the system
val notificationManager: NotificationManager =
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private fun createNotification(
context: Context,
extensionNames: List<String>
): Notification? {
try {
if (extensionNames.isEmpty()) return null
val content = extensionNames.joinToString(", ")
// main { // DON'T WANT TO SLOW IT DOWN
val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID)
.setAutoCancel(false)
.setColorized(true)
.setOnlyAlertOnce(true)
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setContentTitle(context.getString(R.string.plugins_updated, extensionNames.size))
.setSmallIcon(R.drawable.ic_baseline_extension_24)
.setStyle(NotificationCompat.BigTextStyle()
.bigText(content))
.setContentText(content)
if (!hasCreatedNotChanel) {
context.createNotificationChannel()
}
val notification = builder.build()
with(NotificationManagerCompat.from(context)) {
// notificationId is a unique int for each notification that you must define
notify((System.currentTimeMillis()/1000).toInt(), notification)
}
return notification
} catch (e: Exception) {
logError(e)
return null
}
}
} }

View File

@ -40,13 +40,15 @@ class WebviewFragment : Fragment() {
return super.shouldOverrideUrlLoading(view, request) return super.shouldOverrideUrlLoading(view, request)
} }
} }
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
web_view.settings.javaScriptEnabled = true web_view.settings.javaScriptEnabled = true
web_view.settings.userAgentString = USER_AGENT web_view.settings.userAgentString = USER_AGENT
web_view.settings.domStorageEnabled = true web_view.settings.domStorageEnabled = true
// WebView.setWebContentsDebuggingEnabled(true)
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString
// web_view.settings.userAgentString = USER_AGENT
web_view.loadUrl(url) web_view.loadUrl(url)
} }

View File

@ -611,6 +611,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_lock?.isGone = !isShowing player_lock?.isGone = !isShowing
//player_media_route_button?.isClickable = !isGone //player_media_route_button?.isClickable = !isGone
player_go_back_holder?.isGone = isGone player_go_back_holder?.isGone = isGone
player_sources_btt?.isGone = isGone
} }
private fun updateLockUI() { private fun updateLockUI() {

View File

@ -60,7 +60,7 @@ class EpisodeAdapter(
private val clickCallback: (EpisodeClickEvent) -> Unit, private val clickCallback: (EpisodeClickEvent) -> Unit,
private val downloadClickCallback: (DownloadClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var cardList: MutableList<ResultEpisode> = mutableListOf() var cardList: MutableList<ResultEpisode> = mutableListOf()
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet() private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? { private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
@ -239,7 +239,6 @@ class EpisodeAdapter(
itemView.setOnLongClickListener { itemView.setOnLongClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
return@setOnLongClickListener true return@setOnLongClickListener true
} }

View File

@ -95,6 +95,7 @@ import kotlinx.android.synthetic.main.fragment_result.result_vpn
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_result_tv.* import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.result_sync.* import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -293,7 +294,7 @@ open class ResultFragment : ResultTrailerPlayer() {
result_reload_connection_open_in_browser?.isVisible = true result_reload_connection_open_in_browser?.isVisible = true
} }
2 -> { 2 -> {
result_bookmark_fab?.isGone = isTvSettings() result_bookmark_fab?.isGone = isTrueTvSettings()
result_bookmark_fab?.extend() result_bookmark_fab?.extend()
//if (result_bookmark_button?.context?.isTrueTvSettings() == true) { //if (result_bookmark_button?.context?.isTrueTvSettings() == true) {
// when { // when {
@ -412,7 +413,39 @@ open class ResultFragment : ResultTrailerPlayer() {
is ResourceSome.Success -> { is ResourceSome.Success -> {
result_episodes?.isVisible = true result_episodes?.isVisible = true
result_episode_loading?.isVisible = false result_episode_loading?.isVisible = false
/*
* Okay so what is this fuckery?
* Basically Android TV will crash if you request a new focus while
* the adapter gets updated.
*
* This means that if you load thumbnails and request a next focus at the same time
* the app will crash without any way to catch it!
*
* How to bypass this?
* This code basically steals the focus for 500ms and puts it in an inescapable view
* then lets out the focus by requesting focus to result_episodes
*/
// Do not use this.isTv, that is the player
val isTv = isTvSettings()
val hasEpisodes =
!(result_episodes?.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty()
if (isTv && hasEpisodes) {
// Make it impossible to focus anywhere else!
temporary_no_focus?.isFocusable = true
temporary_no_focus?.requestFocus()
}
(result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value) (result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value)
if (isTv && hasEpisodes) main {
delay(500)
temporary_no_focus?.isFocusable = false
// This might make some people sad as it changes the focus when leaving an episode :(
result_episodes?.requestFocus()
}
} }
} }
} }
@ -458,7 +491,14 @@ open class ResultFragment : ResultTrailerPlayer() {
val storedData = getStoredData(activity ?: context ?: return) ?: return val storedData = getStoredData(activity ?: context ?: return) ?: return
//viewModel.clear() //viewModel.clear()
viewModel.load(activity, storedData.url ?: return, storedData.apiName, storedData.showFillers, storedData.dubStatus, storedData.start) viewModel.load(
activity,
storedData.url ?: return,
storedData.apiName,
storedData.showFillers,
storedData.dubStatus,
storedData.start
)
} }
} }
@ -916,7 +956,14 @@ open class ResultFragment : ResultTrailerPlayer() {
if (storedData?.url != null) { if (storedData?.url != null) {
result_reload_connectionerror.setOnClickListener { result_reload_connectionerror.setOnClickListener {
viewModel.load(activity, storedData.url, storedData.apiName, storedData.showFillers, storedData.dubStatus, storedData.start) viewModel.load(
activity,
storedData.url,
storedData.apiName,
storedData.showFillers,
storedData.dubStatus,
storedData.start
)
} }
result_reload_connection_open_in_browser?.setOnClickListener { result_reload_connection_open_in_browser?.setOnClickListener {
@ -952,7 +999,14 @@ open class ResultFragment : ResultTrailerPlayer() {
if (restart || !viewModel.hasLoaded()) { if (restart || !viewModel.hasLoaded()) {
//viewModel.clear() //viewModel.clear()
viewModel.load(activity, storedData.url, storedData.apiName, storedData.showFillers, storedData.dubStatus, storedData.start) viewModel.load(
activity,
storedData.url,
storedData.apiName,
storedData.showFillers,
storedData.dubStatus,
storedData.start
)
} }
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -420,14 +420,14 @@
<FrameLayout <FrameLayout
android:nextFocusRight="@id/result_bookmark_button"
android:id="@+id/result_movie_progress_downloaded_holder" android:id="@+id/result_movie_progress_downloaded_holder"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:layout_weight="1" android:layout_weight="1"
android:minWidth="250dp"> android:minWidth="250dp"
android:nextFocusRight="@id/result_bookmark_button">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/result_download_movie" android:id="@+id/result_download_movie"
@ -510,17 +510,17 @@
</FrameLayout> </FrameLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:nextFocusLeft="@id/result_movie_progress_downloaded_holder"
android:nextFocusDown="@id/result_resume_series_button_play"
android:id="@+id/result_bookmark_button" android:id="@+id/result_bookmark_button"
style="@style/BlackButton" style="@style/BlackButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:layout_weight="1" android:layout_weight="1"
android:minWidth="250dp" android:minWidth="250dp"
android:nextFocusLeft="@id/result_movie_progress_downloaded_holder"
android:nextFocusDown="@id/result_resume_series_button_play"
android:text="@string/type_none" android:text="@string/type_none"
android:visibility="visible" /> android:visibility="visible" />
</LinearLayout> </LinearLayout>
@ -753,6 +753,16 @@
android:orientation="horizontal" android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/result_episode" /> tools:listitem="@layout/result_episode" />
<View
android:id="@+id/temporary_no_focus"
android:layout_width="1dp"
android:layout_height="1dp"
android:focusable="false"
android:nextFocusLeft="@id/temporary_no_focus"
android:nextFocusRight="@id/temporary_no_focus"
android:nextFocusUp="@id/temporary_no_focus"
android:nextFocusDown="@id/temporary_no_focus" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -455,4 +455,5 @@
<string name="extension_types">Wspierane</string> <string name="extension_types">Wspierane</string>
<string name="extension_language">Język</string> <string name="extension_language">Język</string>
<string name="extension_install_first">Najpierw zainstaluj rozszerzenie</string> <string name="extension_install_first">Najpierw zainstaluj rozszerzenie</string>
<string name="plugins_updated">Zaaktualizowano %d rozszerzeń</string>
</resources> </resources>

View File

@ -601,6 +601,7 @@
<string name="plugins_downloaded" formatted="true">Downloaded: %d</string> <string name="plugins_downloaded" formatted="true">Downloaded: %d</string>
<string name="plugins_disabled" formatted="true">Disabled: %d</string> <string name="plugins_disabled" formatted="true">Disabled: %d</string>
<string name="plugins_not_downloaded" formatted="true">Not downloaded: %d</string> <string name="plugins_not_downloaded" formatted="true">Not downloaded: %d</string>
<string name="plugins_updated" formatted="true">Updated %d plugins</string>
<string name="blank_repo_message">Add a repository to install site extensions</string> <string name="blank_repo_message">Add a repository to install site extensions</string>
<string name="view_public_repositories_button">View community repositories</string> <string name="view_public_repositories_button">View community repositories</string>
<string name="view_public_repositories_button_short">Public list</string> <string name="view_public_repositories_button_short">Public list</string>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB