v1.0.1 업데이트
- 정렬 순서 wr_datetime DESC로 변경 (최신순 정렬) - 런처 아이콘 개선 (Netflix 스타일: 빨간 배경 + 흰 아이콘) - 웹뷰 동영상 재생 최적화 (_LAYER_TYPE_NONE, setInterval 제거) - 설정 버튼 카테고리 하단 배치 - 메모리 누수 수정 (WebView cleanup)
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId "com.example.tvmon"
|
applicationId "com.example.tvmon"
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 1
|
versionCode 2
|
||||||
versionName "1.0.0"
|
versionName "1.0.1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,19 +55,35 @@
|
|||||||
android:theme="@style/Theme.Tvmon.Search" />
|
android:theme="@style/Theme.Tvmon.Search" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.search.SearchActivity"
|
android:name=".ui.search.SearchActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/search_title"
|
android:label="@string/search_title"
|
||||||
android:screenOrientation="landscape"
|
android:screenOrientation="landscape"
|
||||||
android:theme="@style/Theme.Tvmon.Search">
|
android:theme="@style/Theme.Tvmon.Search">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
</application>
|
<activity
|
||||||
|
android:name=".ui.settings.SettingsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:screenOrientation="landscape"
|
||||||
|
android:theme="@style/Theme.Tvmon" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import com.example.tvmon.data.local.entity.CategoryContent
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface CategoryContentDao {
|
interface CategoryContentDao {
|
||||||
@Query("SELECT * FROM category_content WHERE categoryKey = :categoryKey ORDER BY cachedAt ASC")
|
@Query("SELECT * FROM category_content WHERE categoryKey = :categoryKey")
|
||||||
suspend fun getByCategory(categoryKey: String): List<CategoryContent>
|
suspend fun getByCategory(categoryKey: String): List<CategoryContent>
|
||||||
|
|
||||||
@Query("SELECT * FROM category_content WHERE categoryKey = :categoryKey AND pageNumber <= :page ORDER BY cachedAt ASC")
|
@Query("SELECT * FROM category_content WHERE categoryKey = :categoryKey AND pageNumber <= :page")
|
||||||
suspend fun getByCategoryUntilPage(categoryKey: String, page: Int): List<CategoryContent>
|
suspend fun getByCategoryUntilPage(categoryKey: String, page: Int): List<CategoryContent>
|
||||||
|
|
||||||
@Query("SELECT MAX(pageNumber) FROM category_content WHERE categoryKey = :categoryKey")
|
@Query("SELECT MAX(pageNumber) FROM category_content WHERE categoryKey = :categoryKey")
|
||||||
suspend fun getMaxPage(categoryKey: String): Int?
|
suspend fun getMaxPage(categoryKey: String): Int?
|
||||||
|
|||||||
@@ -142,17 +142,19 @@ private fun extractSeriesUrl(url: String): String {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCategory(categoryKey: String, page: Int = 1): CategoryResult = withContext(Dispatchers.IO) {
|
suspend fun getCategory(categoryKey: String, page: Int = 1): CategoryResult = withContext(Dispatchers.IO) {
|
||||||
val catInfo = CATEGORIES[categoryKey] ?: return@withContext CategoryResult(
|
val catInfo = CATEGORIES[categoryKey] ?: return@withContext CategoryResult(
|
||||||
success = false,
|
success = false,
|
||||||
category = "Unknown",
|
category = "Unknown",
|
||||||
items = emptyList(),
|
items = emptyList(),
|
||||||
page = page,
|
page = page,
|
||||||
pagination = Pagination(1, 1)
|
pagination = Pagination(1, 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
val url = if (page == 1) "$BASE_URL${catInfo.path}" else "$BASE_URL${catInfo.path}?page=$page"
|
// Sort by wr_datetime (latest first) - same as website default sorting
|
||||||
val html = get(url) ?: return@withContext CategoryResult(
|
val sortParam = "sst=wr_datetime&sod=desc"
|
||||||
|
val url = if (page == 1) "$BASE_URL${catInfo.path}?$sortParam" else "$BASE_URL${catInfo.path}?$sortParam&page=$page"
|
||||||
|
val html = get(url) ?: return@withContext CategoryResult(
|
||||||
success = false,
|
success = false,
|
||||||
category = catInfo.name,
|
category = catInfo.name,
|
||||||
items = emptyList(),
|
items = emptyList(),
|
||||||
|
|||||||
@@ -15,21 +15,24 @@ import com.example.tvmon.data.repository.WatchHistoryRepository
|
|||||||
import com.example.tvmon.data.scraper.TvmonScraper
|
import com.example.tvmon.data.scraper.TvmonScraper
|
||||||
import com.example.tvmon.ui.detail.DetailsActivity
|
import com.example.tvmon.ui.detail.DetailsActivity
|
||||||
import com.example.tvmon.ui.presenter.ContentCardPresenter
|
import com.example.tvmon.ui.presenter.ContentCardPresenter
|
||||||
|
import com.example.tvmon.ui.settings.SettingsActivity
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemViewSelectedListener {
|
class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemViewSelectedListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "TVMON_MAIN"
|
private const val TAG = "TVMON_MAIN"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val categoryCacheRepository: CategoryCacheRepository by inject()
|
data class SettingsItem(val title: String = "설정")
|
||||||
private val watchHistoryRepository: WatchHistoryRepository by inject()
|
|
||||||
private val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
|
private val categoryCacheRepository: CategoryCacheRepository by inject()
|
||||||
private val categoryRowAdapters = mutableMapOf<String, ArrayObjectAdapter>()
|
private val watchHistoryRepository: WatchHistoryRepository by inject()
|
||||||
private val loadingStates = mutableMapOf<String, Boolean>()
|
private val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
|
||||||
private var isDataLoaded = false
|
private val categoryRowAdapters = mutableMapOf<String, ArrayObjectAdapter>()
|
||||||
|
private val loadingStates = mutableMapOf<String, Boolean>()
|
||||||
|
private var isDataLoaded = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -99,13 +102,26 @@ private fun setupUI() {
|
|||||||
if (success) successCount++ else failCount++
|
if (success) successCount++ else failCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.w(TAG, "=== loadCategories COMPLETE: success=$successCount, fail=$failCount ===")
|
Log.w(TAG, "=== loadCategories COMPLETE: success=$successCount, fail=$failCount ===")
|
||||||
isDataLoaded = true
|
isDataLoaded = true
|
||||||
|
|
||||||
if (rowsAdapter.size() == 0) {
|
if (rowsAdapter.size() == 0) {
|
||||||
Toast.makeText(requireContext(), "카테고리를 불러오지 못했습니다", Toast.LENGTH_LONG).show()
|
Toast.makeText(requireContext(), "카테고리를 불러오지 못했습니다", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
addSettingsRow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addSettingsRow() {
|
||||||
|
val settingsAdapter = ArrayObjectAdapter(ContentCardPresenter())
|
||||||
|
settingsAdapter.add(SettingsItem())
|
||||||
|
val settingsHeader = HeaderItem("설정")
|
||||||
|
val settingsRow = ListRow(settingsHeader, settingsAdapter)
|
||||||
|
rowsAdapter.add(settingsRow)
|
||||||
|
Log.w(TAG, "ADDED SETTINGS ROW")
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun loadCategoryWithCache(category: com.example.tvmon.data.model.Category): Boolean {
|
private suspend fun loadCategoryWithCache(category: com.example.tvmon.data.model.Category): Boolean {
|
||||||
return try {
|
return try {
|
||||||
@@ -139,26 +155,30 @@ private fun setupUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClicked(
|
override fun onItemClicked(
|
||||||
itemViewHolder: Presenter.ViewHolder?,
|
itemViewHolder: Presenter.ViewHolder?,
|
||||||
item: Any?,
|
item: Any?,
|
||||||
rowViewHolder: RowPresenter.ViewHolder?,
|
rowViewHolder: RowPresenter.ViewHolder?,
|
||||||
row: Row?
|
row: Row?
|
||||||
) {
|
) {
|
||||||
Log.w(TAG, "=== onItemClicked: item=$item ===")
|
Log.w(TAG, "=== onItemClicked: item=$item ===")
|
||||||
when (item) {
|
when (item) {
|
||||||
is Content -> {
|
is Content -> {
|
||||||
Log.w(TAG, "Content clicked: ${item.title}, url=${item.url}")
|
Log.w(TAG, "Content clicked: ${item.title}, url=${item.url}")
|
||||||
val intent = Intent(requireContext(), DetailsActivity::class.java).apply {
|
val intent = Intent(requireContext(), DetailsActivity::class.java).apply {
|
||||||
putExtra(DetailsActivity.EXTRA_CONTENT, item)
|
putExtra(DetailsActivity.EXTRA_CONTENT, item)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
else -> {
|
is SettingsItem -> {
|
||||||
Log.w(TAG, "Unknown item type: ${item?.javaClass?.simpleName}")
|
Log.w(TAG, "Settings clicked")
|
||||||
}
|
startActivity(Intent(requireContext(), SettingsActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
else -> {
|
||||||
|
Log.w(TAG, "Unknown item type: ${item?.javaClass?.simpleName}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onItemSelected(
|
override fun onItemSelected(
|
||||||
itemViewHolder: Presenter.ViewHolder?,
|
itemViewHolder: Presenter.ViewHolder?,
|
||||||
|
|||||||
@@ -153,152 +153,158 @@ class PlaybackActivity : AppCompatActivity() {
|
|||||||
webView.loadUrl(url)
|
webView.loadUrl(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
private fun setupWebView() {
|
private fun setupWebView() {
|
||||||
val webSettings: WebSettings = webView.settings
|
val webSettings: WebSettings = webView.settings
|
||||||
|
|
||||||
webSettings.javaScriptEnabled = true
|
webSettings.javaScriptEnabled = true
|
||||||
webSettings.domStorageEnabled = true
|
webSettings.domStorageEnabled = true
|
||||||
webSettings.databaseEnabled = true
|
webSettings.databaseEnabled = true
|
||||||
webSettings.allowFileAccess = true
|
webSettings.allowFileAccess = true
|
||||||
webSettings.allowContentAccess = true
|
webSettings.allowContentAccess = true
|
||||||
webSettings.mediaPlaybackRequiresUserGesture = false
|
webSettings.mediaPlaybackRequiresUserGesture = false
|
||||||
webSettings.loadsImagesAutomatically = true
|
webSettings.loadsImagesAutomatically = true
|
||||||
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||||
webSettings.userAgentString = USER_AGENT
|
webSettings.userAgentString = USER_AGENT
|
||||||
webSettings.useWideViewPort = true
|
webSettings.useWideViewPort = true
|
||||||
webSettings.loadWithOverviewMode = true
|
webSettings.loadWithOverviewMode = true
|
||||||
webSettings.cacheMode = WebSettings.LOAD_DEFAULT
|
webSettings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
webSettings.allowUniversalAccessFromFileURLs = true
|
webSettings.allowUniversalAccessFromFileURLs = true
|
||||||
webSettings.allowFileAccessFromFileURLs = true
|
webSettings.allowFileAccessFromFileURLs = true
|
||||||
|
|
||||||
webView.addJavascriptInterface(object {
|
// Video playback optimizations
|
||||||
@android.webkit.JavascriptInterface
|
webSettings.mediaPlaybackRequiresUserGesture = false
|
||||||
fun hideLoadingOverlay() {
|
webSettings.blockNetworkImage = false
|
||||||
runOnUiThread {
|
|
||||||
loadingOverlay.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@android.webkit.JavascriptInterface
|
|
||||||
fun isVideoPlaying() {
|
|
||||||
webView.evaluateJavascript(
|
|
||||||
"""
|
|
||||||
(function() {
|
|
||||||
var videos = document.querySelectorAll('video');
|
|
||||||
var playing = false;
|
|
||||||
videos.forEach(function(v) {
|
|
||||||
if (!v.paused && v.currentTime > 0) {
|
|
||||||
playing = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return playing ? 'playing' : 'not_playing';
|
|
||||||
})();
|
|
||||||
""".trimIndent()
|
|
||||||
) { result ->
|
|
||||||
android.util.Log.i("PlaybackActivity", "Video playing status: $result")
|
|
||||||
if (result == "\"playing\"") {
|
|
||||||
runOnUiThread {
|
|
||||||
loadingOverlay.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@android.webkit.JavascriptInterface
|
|
||||||
fun onVideoPlay() {
|
|
||||||
runOnUiThread {
|
|
||||||
loadingOverlay.visibility = View.GONE
|
|
||||||
android.util.Log.d("PlaybackActivity", "Video started playing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, "Android")
|
|
||||||
|
|
||||||
val cookieManager = CookieManager.getInstance()
|
webView.addJavascriptInterface(object {
|
||||||
cookieManager.setAcceptCookie(true)
|
@android.webkit.JavascriptInterface
|
||||||
cookieManager.setAcceptThirdPartyCookies(webView, true)
|
fun hideLoadingOverlay() {
|
||||||
|
runOnUiThread {
|
||||||
|
loadingOverlay.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
webView.webChromeClient = object : WebChromeClient() {
|
@android.webkit.JavascriptInterface
|
||||||
override fun onConsoleMessage(cm: ConsoleMessage?): Boolean {
|
fun isVideoPlaying() {
|
||||||
val message = cm?.message() ?: return false
|
webView.evaluateJavascript(
|
||||||
android.util.Log.d("WebViewConsole", "[$${cm.sourceId()}:${cm.lineNumber()}] $message")
|
"""
|
||||||
|
(function() {
|
||||||
|
var videos = document.querySelectorAll('video');
|
||||||
|
var playing = false;
|
||||||
|
videos.forEach(function(v) {
|
||||||
|
if (!v.paused && v.currentTime > 0) {
|
||||||
|
playing = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return playing ? 'playing' : 'not_playing';
|
||||||
|
})();
|
||||||
|
""".trimIndent()
|
||||||
|
) { result ->
|
||||||
|
android.util.Log.i("PlaybackActivity", "Video playing status: $result")
|
||||||
|
if (result == "\"playing\"") {
|
||||||
|
runOnUiThread {
|
||||||
|
loadingOverlay.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@android.webkit.JavascriptInterface
|
||||||
|
fun onVideoPlay() {
|
||||||
|
runOnUiThread {
|
||||||
|
loadingOverlay.visibility = View.GONE
|
||||||
|
android.util.Log.d("PlaybackActivity", "Video started playing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "Android")
|
||||||
|
|
||||||
if (message.contains("player") || message.contains("video") || message.contains(".m3u8") || message.contains(".mp4")) {
|
val cookieManager = CookieManager.getInstance()
|
||||||
android.util.Log.i("WebViewConsole", "Player info: $message")
|
cookieManager.setAcceptCookie(true)
|
||||||
}
|
cookieManager.setAcceptThirdPartyCookies(webView, true)
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPermissionRequest(request: android.webkit.PermissionRequest?) {
|
webView.webChromeClient = object : WebChromeClient() {
|
||||||
android.util.Log.d("PlaybackActivity", "Permission request: ${request?.resources?.joinToString()}")
|
override fun onConsoleMessage(cm: ConsoleMessage?): Boolean {
|
||||||
request?.grant(request.resources)
|
val message = cm?.message() ?: return false
|
||||||
}
|
android.util.Log.d("WebViewConsole", "[$${cm.sourceId()}:${cm.lineNumber()}] $message")
|
||||||
}
|
|
||||||
|
|
||||||
webView.webViewClient = object : WebViewClient() {
|
if (message.contains("player") || message.contains("video") || message.contains(".m3u8") || message.contains(".mp4")) {
|
||||||
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
android.util.Log.i("WebViewConsole", "Player info: $message")
|
||||||
val url = request?.url?.toString() ?: ""
|
}
|
||||||
|
return true
|
||||||
if (url.contains(".css") || url.contains(".js") && url.contains("tvmon.site")) {
|
}
|
||||||
try {
|
|
||||||
val conn = java.net.URL(url).openConnection() as java.net.HttpURLConnection
|
|
||||||
conn.requestMethod = "GET"
|
|
||||||
conn.connectTimeout = 8000
|
|
||||||
conn.readTimeout = 8000
|
|
||||||
conn.setRequestProperty("User-Agent", USER_AGENT)
|
|
||||||
conn.setRequestProperty("Referer", "https://tvmon.site/")
|
|
||||||
|
|
||||||
val contentType = conn.contentType ?: "text/plain"
|
|
||||||
val encoding = conn.contentEncoding ?: "UTF-8"
|
|
||||||
val inputStream = conn.inputStream
|
|
||||||
val content = inputStream.bufferedReader().use { it.readText() }
|
|
||||||
inputStream.close()
|
|
||||||
|
|
||||||
var modifiedContent = content
|
|
||||||
|
|
||||||
if (contentType.contains("text/html", true)) {
|
|
||||||
modifiedContent = injectFullscreenStyles(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
return WebResourceResponse(
|
|
||||||
contentType, encoding,
|
|
||||||
200, "OK",
|
|
||||||
mapOf("Access-Control-Allow-Origin" to "*"),
|
|
||||||
ByteArrayInputStream(modifiedContent.toByteArray(charset(encoding)))
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
android.util.Log.w("PlaybackActivity", "Intercept failed for $url: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
|
override fun onPermissionRequest(request: android.webkit.PermissionRequest?) {
|
||||||
super.onPageStarted(view, url, favicon)
|
android.util.Log.d("PlaybackActivity", "Permission request: ${request?.resources?.joinToString()}")
|
||||||
runOnUiThread {
|
request?.grant(request.resources)
|
||||||
loadingOverlay.visibility = View.VISIBLE
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
webView.webViewClient = object : WebViewClient() {
|
||||||
super.onPageFinished(view, url)
|
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
||||||
android.util.Log.i("PlaybackActivity", "Page finished: $url")
|
val url = request?.url?.toString() ?: ""
|
||||||
|
|
||||||
handler.postDelayed({
|
if (url.contains(".css") || url.contains(".js") && url.contains("tvmon.site")) {
|
||||||
injectFullscreenScript()
|
try {
|
||||||
injectEnhancedAutoPlayScript()
|
val conn = java.net.URL(url).openConnection() as java.net.HttpURLConnection
|
||||||
}, 200)
|
conn.requestMethod = "GET"
|
||||||
|
conn.connectTimeout = 8000
|
||||||
|
conn.readTimeout = 8000
|
||||||
|
conn.setRequestProperty("User-Agent", USER_AGENT)
|
||||||
|
conn.setRequestProperty("Referer", "https://tvmon.site/")
|
||||||
|
|
||||||
handler.postDelayed({
|
val contentType = conn.contentType ?: "text/plain"
|
||||||
runOnUiThread {
|
val encoding = conn.contentEncoding ?: "UTF-8"
|
||||||
loadingOverlay.visibility = View.GONE
|
val inputStream = conn.inputStream
|
||||||
}
|
val content = inputStream.bufferedReader().use { it.readText() }
|
||||||
}, 1000)
|
inputStream.close()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
var modifiedContent = content
|
||||||
}
|
|
||||||
|
if (contentType.contains("text/html", true)) {
|
||||||
|
modifiedContent = injectFullscreenStyles(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebResourceResponse(
|
||||||
|
contentType, encoding,
|
||||||
|
200, "OK",
|
||||||
|
mapOf("Access-Control-Allow-Origin" to "*"),
|
||||||
|
ByteArrayInputStream(modifiedContent.toByteArray(charset(encoding)))
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.w("PlaybackActivity", "Intercept failed for $url: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
runOnUiThread {
|
||||||
|
loadingOverlay.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
android.util.Log.i("PlaybackActivity", "Page finished: $url")
|
||||||
|
|
||||||
|
// Reduced delays and calls to minimize memory pressure
|
||||||
|
handler.postDelayed({
|
||||||
|
injectFullscreenScript()
|
||||||
|
injectEnhancedAutoPlayScript()
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
handler.postDelayed({
|
||||||
|
runOnUiThread {
|
||||||
|
loadingOverlay.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use hardware layer only when needed, not always
|
||||||
|
webView.setLayerType(View.LAYER_TYPE_NONE, null)
|
||||||
|
}
|
||||||
|
|
||||||
private fun injectFullscreenStyles(html: String): String {
|
private fun injectFullscreenStyles(html: String): String {
|
||||||
val styleInjection = """
|
val styleInjection = """
|
||||||
@@ -449,12 +455,10 @@ class PlaybackActivity : AppCompatActivity() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
makePlayerFullscreen();
|
makePlayerFullscreen();
|
||||||
setTimeout(makePlayerFullscreen, 500);
|
setTimeout(makePlayerFullscreen, 500);
|
||||||
setTimeout(makePlayerFullscreen, 1500);
|
setTimeout(makePlayerFullscreen, 2000);
|
||||||
setTimeout(makePlayerFullscreen, 3000);
|
})();
|
||||||
setInterval(makePlayerFullscreen, 5000);
|
|
||||||
})();
|
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.leanback.widget.ImageCardView
|
import androidx.leanback.widget.ImageCardView
|
||||||
import androidx.leanback.widget.Presenter
|
import androidx.leanback.widget.Presenter
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||||
import com.example.tvmon.R
|
import com.example.tvmon.R
|
||||||
import com.example.tvmon.data.model.Content
|
import com.example.tvmon.data.model.Content
|
||||||
import com.example.tvmon.data.model.Category
|
import com.example.tvmon.data.model.Category
|
||||||
@@ -68,8 +70,10 @@ class ContentCardPresenter : Presenter() {
|
|||||||
if (content.thumbnail.isNotBlank()) {
|
if (content.thumbnail.isNotBlank()) {
|
||||||
Glide.with(cardView.context)
|
Glide.with(cardView.context)
|
||||||
.load(content.thumbnail)
|
.load(content.thumbnail)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
.error(R.drawable.default_background)
|
.error(R.drawable.default_background)
|
||||||
.placeholder(R.drawable.default_background)
|
.placeholder(R.drawable.default_background)
|
||||||
|
.transition(DrawableTransitionOptions.withCrossFade(200))
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.into(cardView.mainImageView)
|
.into(cardView.mainImageView)
|
||||||
} else {
|
} else {
|
||||||
@@ -143,8 +147,10 @@ class SearchCardPresenter : Presenter() {
|
|||||||
if (content.thumbnail.isNotBlank()) {
|
if (content.thumbnail.isNotBlank()) {
|
||||||
Glide.with(cardView.context)
|
Glide.with(cardView.context)
|
||||||
.load(content.thumbnail)
|
.load(content.thumbnail)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
.error(R.drawable.default_background)
|
.error(R.drawable.default_background)
|
||||||
.placeholder(R.drawable.default_background)
|
.placeholder(R.drawable.default_background)
|
||||||
|
.transition(DrawableTransitionOptions.withCrossFade(200))
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.into(cardView.mainImageView)
|
.into(cardView.mainImageView)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package com.example.tvmon.ui.settings
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.example.tvmon.R
|
||||||
|
import com.example.tvmon.util.ApkDownloader
|
||||||
|
import com.example.tvmon.util.UpdateChecker
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import com.example.tvmon.data.repository.CategoryCacheRepository
|
||||||
|
|
||||||
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val categoryCacheRepository: CategoryCacheRepository by inject()
|
||||||
|
|
||||||
|
private lateinit var tvAppVersion: TextView
|
||||||
|
private lateinit var tvLatestVersion: TextView
|
||||||
|
private lateinit var btnClearCache: Button
|
||||||
|
private lateinit var btnCheckUpdate: Button
|
||||||
|
private lateinit var btnBack: Button
|
||||||
|
|
||||||
|
private var latestVersionInfo: com.example.tvmon.util.VersionInfo? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
window.setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
|
)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_settings)
|
||||||
|
|
||||||
|
initViews()
|
||||||
|
loadSettings()
|
||||||
|
setupListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initViews() {
|
||||||
|
tvAppVersion = findViewById(R.id.tv_app_version)
|
||||||
|
tvLatestVersion = findViewById(R.id.tv_latest_version)
|
||||||
|
btnClearCache = findViewById(R.id.btn_clear_cache)
|
||||||
|
btnCheckUpdate = findViewById(R.id.btn_check_update)
|
||||||
|
btnBack = findViewById(R.id.btn_back)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
val versionName = UpdateChecker.getCurrentVersionName(this)
|
||||||
|
val versionCode = UpdateChecker.getCurrentVersionCode(this)
|
||||||
|
tvAppVersion.text = "v$versionName (build $versionCode)"
|
||||||
|
|
||||||
|
loadLatestVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLatestVersion() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
latestVersionInfo = UpdateChecker.fetchLatestVersion()
|
||||||
|
|
||||||
|
if (latestVersionInfo != null) {
|
||||||
|
val currentVersionCode = UpdateChecker.getCurrentVersionCode(this@SettingsActivity)
|
||||||
|
if (latestVersionInfo!!.versionCode > currentVersionCode) {
|
||||||
|
tvLatestVersion.text = "최신 버전: v${latestVersionInfo!!.versionName} (업데이트 가능)"
|
||||||
|
tvLatestVersion.setTextColor(getColor(R.color.accent))
|
||||||
|
} else {
|
||||||
|
tvLatestVersion.text = "최신 버전: v${latestVersionInfo!!.versionName} (최신)"
|
||||||
|
tvLatestVersion.setTextColor(getColor(R.color.netflix_light_gray))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tvLatestVersion.text = "최신 버전: 확인 실패"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
tvLatestVersion.text = "최신 버전: 확인 실패"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupListeners() {
|
||||||
|
btnClearCache.setOnClickListener {
|
||||||
|
showClearCacheDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnCheckUpdate.setOnClickListener {
|
||||||
|
checkForUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnBack.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showClearCacheDialog() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("캐시 삭제")
|
||||||
|
.setMessage("모든 캐시 데이터를 삭제하시겠습니까?\n\n삭제 항목:\n- 영화/에피소드 정보\n- 이미지 캐시")
|
||||||
|
.setPositiveButton("삭제") { _, _ ->
|
||||||
|
clearAllCache()
|
||||||
|
}
|
||||||
|
.setNegativeButton("취소", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearAllCache() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
categoryCacheRepository.clearCache()
|
||||||
|
|
||||||
|
com.bumptech.glide.Glide.get(this@SettingsActivity).clearMemory()
|
||||||
|
com.bumptech.glide.Glide.get(this@SettingsActivity).clearDiskCache()
|
||||||
|
|
||||||
|
Toast.makeText(this@SettingsActivity, "캐시가 삭제되었습니다", Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@SettingsActivity, "캐시 삭제 실패: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkForUpdate() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val latestVersion = UpdateChecker.fetchLatestVersion()
|
||||||
|
|
||||||
|
if (latestVersion != null) {
|
||||||
|
if (UpdateChecker.needsUpdate(this@SettingsActivity, latestVersion)) {
|
||||||
|
showUpdateDialog(latestVersion)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@SettingsActivity, "최신 버전입니다 (v${latestVersion.versionName})", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@SettingsActivity, "업데이트 정보를 가져올 수 없습니다", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@SettingsActivity, "업데이트 확인 실패: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUpdateDialog(versionInfo: com.example.tvmon.util.VersionInfo) {
|
||||||
|
val message = buildString {
|
||||||
|
append("현재 버전: ${UpdateChecker.getCurrentVersionName(this@SettingsActivity)}\n")
|
||||||
|
append("최신 버전: ${versionInfo.versionName}\n\n")
|
||||||
|
append(versionInfo.updateMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("업데이트 알림")
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton("업데이트") { _, _ ->
|
||||||
|
Toast.makeText(this, "업데이트 다운로드 시작...", Toast.LENGTH_SHORT).show()
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val apkFile = ApkDownloader.downloadApkDirect(this@SettingsActivity, versionInfo.apkUrl)
|
||||||
|
if (apkFile != null) {
|
||||||
|
ApkDownloader.installApk(this@SettingsActivity, apkFile)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@SettingsActivity, "다운로드 실패", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@SettingsActivity, "다운로드 오류: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton("나중에", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.example.tvmon.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object ApkDownloader {
|
||||||
|
private const val TAG = "ApkDownloader"
|
||||||
|
private const val APK_FILE_NAME = "tvmon_update.apk"
|
||||||
|
private const val CONNECT_TIMEOUT = 30L
|
||||||
|
private const val READ_TIMEOUT = 60L
|
||||||
|
|
||||||
|
private val okHttpClient by lazy {
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadApkDirect(context: Context, apkUrl: String): File? = withContext(Dispatchers.IO) {
|
||||||
|
var response: Response? = null
|
||||||
|
var outputStream: FileOutputStream? = null
|
||||||
|
try {
|
||||||
|
android.util.Log.i(TAG, "Starting download from: $apkUrl")
|
||||||
|
|
||||||
|
val apkFile = getApkFile(context)
|
||||||
|
android.util.Log.i(TAG, "APK file path: ${apkFile.absolutePath}")
|
||||||
|
|
||||||
|
if (apkFile.exists()) {
|
||||||
|
android.util.Log.i(TAG, "Deleting existing APK file")
|
||||||
|
apkFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
apkFile.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(apkUrl)
|
||||||
|
.header("User-Agent", "Mozilla/5.0 (Linux; Android TV) AppleWebKit/537.36")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
android.util.Log.i(TAG, "Executing request...")
|
||||||
|
|
||||||
|
response = okHttpClient.newCall(request).execute()
|
||||||
|
android.util.Log.i(TAG, "Response received: ${response.code} ${response.message}")
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
android.util.Log.e(TAG, "Download failed: ${response.code} ${response.message}")
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseBody: ResponseBody = response.body ?: return@withContext null
|
||||||
|
val contentLength = responseBody.contentLength()
|
||||||
|
|
||||||
|
android.util.Log.i(TAG, "Starting file download...")
|
||||||
|
outputStream = FileOutputStream(apkFile)
|
||||||
|
val inputStream = responseBody.byteStream()
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var bytesRead: Int
|
||||||
|
var totalBytesRead = 0L
|
||||||
|
|
||||||
|
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead)
|
||||||
|
totalBytesRead += bytesRead
|
||||||
|
|
||||||
|
if (contentLength > 0) {
|
||||||
|
val progress = (totalBytesRead * 100 / contentLength).toInt()
|
||||||
|
if (progress % 10 == 0) {
|
||||||
|
android.util.Log.d(TAG, "Download progress: $progress%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream.flush()
|
||||||
|
android.util.Log.i(TAG, "APK download completed: ${apkFile.absolutePath}, size: ${apkFile.length()}")
|
||||||
|
|
||||||
|
return@withContext apkFile
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e(TAG, "Download failed: ${e.message}", e)
|
||||||
|
return@withContext null
|
||||||
|
} finally {
|
||||||
|
outputStream?.close()
|
||||||
|
response?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installApk(context: Context, apkFile: File) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|
||||||
|
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
apkFile
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Uri.fromFile(apkFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e(TAG, "Failed to install APK: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getApkFile(context: Context): File {
|
||||||
|
return File(context.filesDir, APK_FILE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isApkDownloaded(context: Context): Boolean {
|
||||||
|
return getApkFile(context).exists()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.example.tvmon.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
data class VersionInfo(
|
||||||
|
val versionCode: Int,
|
||||||
|
val versionName: String,
|
||||||
|
val apkUrl: String,
|
||||||
|
val updateMessage: String,
|
||||||
|
val forceUpdate: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
object UpdateChecker {
|
||||||
|
private const val TAG = "UpdateChecker"
|
||||||
|
|
||||||
|
private const val VERSION_JSON_URL = "https://tvmon.site/version.json"
|
||||||
|
private const val APK_BASE_URL = "https://tvmon.site/releases"
|
||||||
|
|
||||||
|
fun getCurrentVersionCode(context: Context): Int {
|
||||||
|
return try {
|
||||||
|
context.packageManager.getPackageInfo(context.packageName, 0).versionCode
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentVersionName(context: Context): String {
|
||||||
|
return try {
|
||||||
|
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "1.0.0"
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
"1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchLatestVersion(): VersionInfo? = withContext(Dispatchers.IO) {
|
||||||
|
fetchLatestVersionDirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchLatestVersionDirect(): VersionInfo? {
|
||||||
|
try {
|
||||||
|
android.util.Log.d(TAG, "Fetching version.json from: $VERSION_JSON_URL")
|
||||||
|
|
||||||
|
val url = java.net.URL(VERSION_JSON_URL)
|
||||||
|
val conn = url.openConnection() as java.net.HttpURLConnection
|
||||||
|
conn.requestMethod = "GET"
|
||||||
|
conn.connectTimeout = 15000
|
||||||
|
conn.readTimeout = 15000
|
||||||
|
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
android.util.Log.d(TAG, "Response Code: $responseCode")
|
||||||
|
|
||||||
|
if (responseCode != 200) {
|
||||||
|
android.util.Log.e(TAG, "HTTP Error: $responseCode")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonStr = conn.inputStream.bufferedReader().readText()
|
||||||
|
android.util.Log.d(TAG, "Response: $jsonStr")
|
||||||
|
|
||||||
|
val json = JSONObject(jsonStr)
|
||||||
|
val versionCode = json.getInt("versionCode")
|
||||||
|
val versionName = json.getString("versionName")
|
||||||
|
val updateMessage = json.optString("updateMessage", "새로운 버전이 있습니다.")
|
||||||
|
val forceUpdate = json.optBoolean("forceUpdate", false)
|
||||||
|
|
||||||
|
val apkUrl = if (json.has("apkUrl") && !json.isNull("apkUrl")) {
|
||||||
|
json.getString("apkUrl")
|
||||||
|
} else {
|
||||||
|
"$APK_BASE_URL/v$versionName/app-release.apk"
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d(TAG, "Version: $versionName, apkUrl: $apkUrl")
|
||||||
|
|
||||||
|
return VersionInfo(
|
||||||
|
versionCode = versionCode,
|
||||||
|
versionName = versionName,
|
||||||
|
apkUrl = apkUrl,
|
||||||
|
updateMessage = updateMessage,
|
||||||
|
forceUpdate = forceUpdate
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e(TAG, "Failed to fetch version: ${e.message}", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun needsUpdate(context: Context, latestVersion: VersionInfo): Boolean {
|
||||||
|
val currentVersionCode = getCurrentVersionCode(context)
|
||||||
|
android.util.Log.i(TAG, "Current: $currentVersionCode, Latest: ${latestVersion.versionCode}")
|
||||||
|
return latestVersion.versionCode > currentVersionCode
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,66 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:shape="rectangle">
|
android:width="320dp"
|
||||||
<solid android:color="#FF6B6B" />
|
android:height="180dp"
|
||||||
<corners android:radius="8dp" />
|
android:viewportWidth="320"
|
||||||
</shape>
|
android:viewportHeight="180">
|
||||||
|
|
||||||
|
<!-- Background with Netflix dark gray -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#141414"
|
||||||
|
android:pathData="M0,0h320v180h-320z"/>
|
||||||
|
|
||||||
|
<!-- TV Screen - Netflix Red -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50914"
|
||||||
|
android:pathData="M110,50h100v70h-100z"/>
|
||||||
|
|
||||||
|
<!-- TV Stand -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50914"
|
||||||
|
android:pathData="M140,125h40v8h-40z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50914"
|
||||||
|
android:pathData="M125,133h70v4h-70z"/>
|
||||||
|
|
||||||
|
<!-- Play Button - White -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M145,70l30,20l-30,20z"/>
|
||||||
|
|
||||||
|
<!-- tvmon Text - White -->
|
||||||
|
<!-- Letter 't' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M100,148h6v-3h-6v-4h10v18h-4v-11h-6z"/>
|
||||||
|
<!-- Letter 'v' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M115,141h4l5,15l5,-15h4l-7,18h-4z"/>
|
||||||
|
<!-- Letter 'm' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M140,159v-18h5v3c1.5,-2 3,-3 5,-3c2,0 3.5,1 4.5,2.5c1.5,-2 3.5,-2.5 5.5,-2.5c4,0 6.5,2.5 6.5,6.5v11.5h-5v-11c0,-2 -1.5,-3.5 -3.5,-3.5s-3.5,1.5 -3.5,3.5v11h-5v-11c0,-2 -1.5,-3.5 -3.5,-3.5s-3.5,1.5 -3.5,3.5v11z"/>
|
||||||
|
<!-- Letter 'o' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M175,148c0,-5.5 4.5,-10 10,-10s10,4.5 10,10s-4.5,10 -10,10s-10,-4.5 -10,-10zm4,0c0,3.5 2.5,6 6,6s6,-2.5 6,-6s-2.5,-6 -6,-6s-6,2.5 -6,6z"/>
|
||||||
|
<!-- Letter 'n' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M205,159v-18h5v3c2,-2 4,-3 6,-3c5,0 8,3 8,8v10h-5v-10c0,-2.5 -2,-4.5 -4.5,-4.5s-4.5,2 -4.5,4.5v10z"/>
|
||||||
|
|
||||||
|
<!-- Right side - decorative lines -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50914"
|
||||||
|
android:fillAlpha="0.3"
|
||||||
|
android:pathData="M250,40h40v4h-40z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50914"
|
||||||
|
android:fillAlpha="0.3"
|
||||||
|
android:pathData="M250,55h35v4h-35z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50914"
|
||||||
|
android:fillAlpha="0.3"
|
||||||
|
android:pathData="M250,70h30v4h-30z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,49 +1,66 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="320dp"
|
android:width="320dp"
|
||||||
android:height="180dp"
|
android:height="180dp"
|
||||||
android:viewportWidth="320"
|
android:viewportWidth="320"
|
||||||
android:viewportHeight="180">
|
android:viewportHeight="180">
|
||||||
|
|
||||||
<!-- Background -->
|
<!-- Background with Netflix dark gray -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#1F1F1F"
|
android:fillColor="#141414"
|
||||||
android:pathData="M0,0h320v180h-320z"/>
|
android:pathData="M0,0h320v180h-320z"/>
|
||||||
|
|
||||||
<!-- Gradient Overlay -->
|
<!-- TV Screen - Netflix Red -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#80000000"
|
android:fillColor="#E50914"
|
||||||
android:pathData="M0,0h320v180h-320z"/>
|
android:pathData="M110,50h100v70h-100z"/>
|
||||||
|
|
||||||
<!-- TV Icon -->
|
<!-- TV Stand -->
|
||||||
<group
|
<path
|
||||||
android:translateX="120"
|
android:fillColor="#E50914"
|
||||||
android:translateY="40">
|
android:pathData="M140,125h40v8h-40z"/>
|
||||||
|
<path
|
||||||
<!-- TV Screen -->
|
android:fillColor="#E50914"
|
||||||
<path
|
android:pathData="M125,133h70v4h-70z"/>
|
||||||
android:fillColor="#FF6B6B"
|
|
||||||
android:pathData="M0,0h80v60h-80z"/>
|
<!-- Play Button - White -->
|
||||||
|
<path
|
||||||
<!-- TV Stand -->
|
android:fillColor="#FFFFFF"
|
||||||
<path
|
android:pathData="M145,70l30,20l-30,20z"/>
|
||||||
android:fillColor="#FF6B6B"
|
|
||||||
android:pathData="M20,65h40v5h-40z"/>
|
<!-- tvmon Text - White -->
|
||||||
<path
|
<!-- Letter 't' -->
|
||||||
android:fillColor="#FF6B6B"
|
<path
|
||||||
android:pathData="M10,70h60v3h-60z"/>
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M100,148h6v-3h-6v-4h10v18h-4v-11h-6z"/>
|
||||||
<!-- Play Icon -->
|
<!-- Letter 'v' -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M30,18l25,17l-25,17z"/>
|
android:pathData="M115,141h4l5,15l5,-15h4l-7,18h-4z"/>
|
||||||
</group>
|
<!-- Letter 'm' -->
|
||||||
|
<path
|
||||||
<!-- App Name -->
|
android:fillColor="#FFFFFF"
|
||||||
<path
|
android:pathData="M140,159v-18h5v3c1.5,-2 3,-3 5,-3c2,0 3.5,1 4.5,2.5c1.5,-2 3.5,-2.5 5.5,-2.5c4,0 6.5,2.5 6.5,6.5v11.5h-5v-11c0,-2 -1.5,-3.5 -3.5,-3.5s-3.5,1.5 -3.5,3.5v11h-5v-11c0,-2 -1.5,-3.5 -3.5,-3.5s-3.5,1.5 -3.5,3.5v11z"/>
|
||||||
android:fillColor="#FFFFFF"
|
<!-- Letter 'o' -->
|
||||||
android:pathData="M120,130h80v4h-80z"/>
|
<path
|
||||||
<path
|
android:fillColor="#FFFFFF"
|
||||||
android:fillColor="#FFFFFF"
|
android:pathData="M175,148c0,-5.5 4.5,-10 10,-10s10,4.5 10,10s-4.5,10 -10,10s-10,-4.5 -10,-10zm4,0c0,3.5 2.5,6 6,6s6,-2.5 6,-6s-2.5,-6 -6,-6s-6,2.5 -6,6z"/>
|
||||||
android:pathData="M140,136h40v3h-40z"/>
|
<!-- Letter 'n' -->
|
||||||
</vector>
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M205,159v-18h5v3c2,-2 4,-3 6,-3c5,0 8,3 8,8v10h-5v-10c0,-2.5 -2,-4.5 -4.5,-4.5s-4.5,2 -4.5,4.5v10z"/>
|
||||||
|
|
||||||
|
<!-- Right side - decorative lines -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50914"
|
||||||
|
android:fillAlpha="0.3"
|
||||||
|
android:pathData="M250,40h40v4h-40z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50914"
|
||||||
|
android:fillAlpha="0.3"
|
||||||
|
android:pathData="M250,55h35v4h-35z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50914"
|
||||||
|
android:fillAlpha="0.3"
|
||||||
|
android:pathData="M250,70h30v4h-30z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,52 +1,47 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
|
||||||
<!-- Background Circle -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF6B6B"
|
|
||||||
android:pathData="M54,54m-40,0a40,40 0,1 1,80 0a40,40 0,1 1,-80 0"/>
|
|
||||||
|
|
||||||
<!-- TV Screen -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M34,38h40v28h-40z"/>
|
|
||||||
|
|
||||||
<!-- TV Stand -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M44,70h20v4h-20z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M40,74h28v2h-28z"/>
|
|
||||||
|
|
||||||
<!-- Play Button -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF6B6B"
|
|
||||||
android:pathData="M50,44l12,8l-12,8z"/>
|
|
||||||
|
|
||||||
<!-- tvmon Text -->
|
<!-- TV Screen with white color -->
|
||||||
<!-- Letter 't' -->
|
<path
|
||||||
<path
|
android:fillColor="#FFFFFF"
|
||||||
android:fillColor="#FFFFFF"
|
android:pathData="M34,38h40v28h-40z"/>
|
||||||
android:pathData="M38,82h4v-2h-4v-2h6v10h-2v-6h-4z"/>
|
|
||||||
<!-- Letter 'v' -->
|
<!-- TV Stand -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M45,78h2l2,6l2,-6h2l-3,8h-2z"/>
|
android:pathData="M44,70h20v4h-20z"/>
|
||||||
<!-- Letter 'm' -->
|
<path
|
||||||
<path
|
android:fillColor="#FFFFFF"
|
||||||
android:fillColor="#FFFFFF"
|
android:pathData="M40,74h28v2h-28z"/>
|
||||||
android:pathData="M54,86v-8h2v1c0.5,-0.7 1.2,-1 2,-1c0.9,0 1.6,0.4 2,1.1c0.5,-0.7 1.3,-1.1 2.2,-1.1c1.6,0 2.8,1.2 2.8,2.8v5.2h-2v-5c0,-0.9 -0.7,-1.5 -1.5,-1.5c-0.9,0 -1.5,0.6 -1.5,1.5v5h-2v-5c0,-0.9 -0.7,-1.5 -1.5,-1.5c-0.9,0 -1.5,0.6 -1.5,1.5v5z"/>
|
|
||||||
<!-- Letter 'o' -->
|
<!-- Play Button - using Netflix red -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#E50914"
|
||||||
android:pathData="M66,82c0,-2.2 1.8,-4 4,-4s4,1.8 4,4s-1.8,4 -4,4s-4,-1.8 -4,-4zm2,0c0,1.1 0.9,2 2,2s2,-0.9 2,-2s-0.9,-2 -2,-2s-2,0.9 -2,2z"/>
|
android:pathData="M50,44l12,8l-12,8z"/>
|
||||||
<!-- Letter 'n' -->
|
|
||||||
<path
|
<!-- tvmon Text - white -->
|
||||||
android:fillColor="#FFFFFF"
|
<!-- Letter 't' -->
|
||||||
android:pathData="M76,86v-8h2v1c0.6,-0.7 1.4,-1 2.3,-1c1.9,0 3.2,1.3 3.2,3.2v4.8h-2v-4.5c0,-1 -0.7,-1.7 -1.7,-1.7c-1,0 -1.8,0.7 -1.8,1.7v4.5z"/>
|
<path
|
||||||
</vector>
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M38,82h4v-2h-4v-2h6v10h-2v-6h-4z"/>
|
||||||
|
<!-- Letter 'v' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M45,78h2l2,6l2,-6h2l-3,8h-2z"/>
|
||||||
|
<!-- Letter 'm' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M54,86v-8h2v1c0.5,-0.7 1.2,-1 2,-1c0.9,0 1.6,0.4 2,1.1c0.5,-0.7 1.3,-1.1 2.2,-1.1c1.6,0 2.8,1.2 2.8,2.8v5.2h-2v-5c0,-0.9 -0.7,-1.5 -1.5,-1.5c-0.9,0 -1.5,0.6 -1.5,1.5v5h-2v-5c0,-0.9 -0.7,-1.5 -1.5,-1.5c-0.9,0 -1.5,0.6 -1.5,1.5v5z"/>
|
||||||
|
<!-- Letter 'o' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M66,82c0,-2.2 1.8,-4 4,-4s4,1.8 4,4s-1.8,4 -4,4s-4,-1.8 -4,-4zm2,0c0,1.1 0.9,2 2,2s2,-0.9 2,-2s-0.9,-2 -2,-2s-2,0.9 -2,2z"/>
|
||||||
|
<!-- Letter 'n' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M76,86v-8h2v1c0.6,-0.7 1.4,-1 2.3,-1c1.9,0 3.2,1.3 3.2,3.2v4.8h-2v-4.5c0,-1 -0.7,-1.7 -1.7,-1.7c-1,0 -1.8,0.7 -1.8,1.7v4.5z"/>
|
||||||
|
</vector>
|
||||||
146
tvmon-app/app/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/netflix_black"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="48dp">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="설정"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="32sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="32dp" />
|
||||||
|
|
||||||
|
<!-- Settings Items -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- App Version -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="앱 버전"
|
||||||
|
android:textColor="@color/netflix_light_gray"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_app_version"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="-"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_latest_version"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="최신 버전: 확인 중..."
|
||||||
|
android:textColor="@color/netflix_light_gray"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginTop="4dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Cache Delete Buttons -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="캐시 관리"
|
||||||
|
android:textColor="@color/netflix_light_gray"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_clear_cache"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="전체 삭제"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:backgroundTint="@color/netflix_medium_gray"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingVertical="12dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Update Check Button -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="업데이트"
|
||||||
|
android:textColor="@color/netflix_light_gray"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_check_update"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="업데이트 확인"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:backgroundTint="@color/accent"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:paddingVertical="12dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Bottom Spacer -->
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<!-- Back Button -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_back"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="뒤로 가기"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:backgroundTint="@color/netflix_medium_gray"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:paddingHorizontal="32dp"
|
||||||
|
android:paddingVertical="16dp"
|
||||||
|
android:layout_gravity="start" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
BIN
tvmon-app/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 757 B |
BIN
tvmon-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 757 B |
BIN
tvmon-app/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 587 B |
BIN
tvmon-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 587 B |
BIN
tvmon-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 938 B |
BIN
tvmon-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 938 B |
BIN
tvmon-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
tvmon-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
tvmon-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
tvmon-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#1F1F1F</color>
|
<color name="ic_launcher_background">#E50914</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
12
tvmon-app/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<files-path
|
||||||
|
name="internal_files"
|
||||||
|
path="." />
|
||||||
|
<cache-path
|
||||||
|
name="cache"
|
||||||
|
path="." />
|
||||||
|
<external-files-path
|
||||||
|
name="external_files"
|
||||||
|
path="." />
|
||||||
|
</paths>
|
||||||
6
tvmon-app/version.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"versionCode": 2,
|
||||||
|
"versionName": "1.0.1",
|
||||||
|
"apkUrl": "https://git.webpluss.net/sanjeok77/tvmon_release/releases/download/v1.0.1/app-release.apk",
|
||||||
|
"updateMessage": "v1.0.1 업데이트\n\n- 정렬 순서 wr_datetime DESC로 변경\n- 런처 아이콘 개선 (Netflix 스타일)\n- 웹뷰 동영상 재생 최적화\n- 설정 버튼 카테고리 하단 배치"
|
||||||
|
}
|
||||||