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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,22 @@
|
|||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ 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")
|
||||||
|
|||||||
@@ -151,7 +151,9 @@ private fun extractSeriesUrl(url: String): String {
|
|||||||
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 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(
|
val html = get(url) ?: return@withContext CategoryResult(
|
||||||
success = false,
|
success = false,
|
||||||
category = catInfo.name,
|
category = catInfo.name,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
|
|||||||
private const val TAG = "TVMON_MAIN"
|
private const val TAG = "TVMON_MAIN"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class SettingsItem(val title: String = "설정")
|
||||||
|
|
||||||
private val categoryCacheRepository: CategoryCacheRepository by inject()
|
private val categoryCacheRepository: CategoryCacheRepository by inject()
|
||||||
private val watchHistoryRepository: WatchHistoryRepository by inject()
|
private val watchHistoryRepository: WatchHistoryRepository by inject()
|
||||||
private val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
|
private val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
|
||||||
@@ -105,6 +108,19 @@ private fun setupUI() {
|
|||||||
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 {
|
||||||
@@ -154,6 +170,10 @@ private fun setupUI() {
|
|||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
is SettingsItem -> {
|
||||||
|
Log.w(TAG, "Settings clicked")
|
||||||
|
startActivity(Intent(requireContext(), SettingsActivity::class.java))
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(TAG, "Unknown item type: ${item?.javaClass?.simpleName}")
|
Log.w(TAG, "Unknown item type: ${item?.javaClass?.simpleName}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,10 @@ class PlaybackActivity : AppCompatActivity() {
|
|||||||
webSettings.allowUniversalAccessFromFileURLs = true
|
webSettings.allowUniversalAccessFromFileURLs = true
|
||||||
webSettings.allowFileAccessFromFileURLs = true
|
webSettings.allowFileAccessFromFileURLs = true
|
||||||
|
|
||||||
|
// Video playback optimizations
|
||||||
|
webSettings.mediaPlaybackRequiresUserGesture = false
|
||||||
|
webSettings.blockNetworkImage = false
|
||||||
|
|
||||||
webView.addJavascriptInterface(object {
|
webView.addJavascriptInterface(object {
|
||||||
@android.webkit.JavascriptInterface
|
@android.webkit.JavascriptInterface
|
||||||
fun hideLoadingOverlay() {
|
fun hideLoadingOverlay() {
|
||||||
@@ -284,20 +288,22 @@ class PlaybackActivity : AppCompatActivity() {
|
|||||||
super.onPageFinished(view, url)
|
super.onPageFinished(view, url)
|
||||||
android.util.Log.i("PlaybackActivity", "Page finished: $url")
|
android.util.Log.i("PlaybackActivity", "Page finished: $url")
|
||||||
|
|
||||||
|
// Reduced delays and calls to minimize memory pressure
|
||||||
handler.postDelayed({
|
handler.postDelayed({
|
||||||
injectFullscreenScript()
|
injectFullscreenScript()
|
||||||
injectEnhancedAutoPlayScript()
|
injectEnhancedAutoPlayScript()
|
||||||
}, 200)
|
}, 300)
|
||||||
|
|
||||||
handler.postDelayed({
|
handler.postDelayed({
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
loadingOverlay.visibility = View.GONE
|
loadingOverlay.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
// 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 {
|
||||||
@@ -451,9 +457,7 @@ 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>
|
||||||
@@ -5,45 +5,62 @@
|
|||||||
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 -->
|
|
||||||
<group
|
|
||||||
android:translateX="120"
|
|
||||||
android:translateY="40">
|
|
||||||
|
|
||||||
<!-- TV Screen -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF6B6B"
|
|
||||||
android:pathData="M0,0h80v60h-80z"/>
|
|
||||||
|
|
||||||
<!-- TV Stand -->
|
<!-- TV Stand -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF6B6B"
|
android:fillColor="#E50914"
|
||||||
android:pathData="M20,65h40v5h-40z"/>
|
android:pathData="M140,125h40v8h-40z"/>
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF6B6B"
|
android:fillColor="#E50914"
|
||||||
android:pathData="M10,70h60v3h-60z"/>
|
android:pathData="M125,133h70v4h-70z"/>
|
||||||
|
|
||||||
<!-- Play Icon -->
|
<!-- Play Button - White -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M30,18l25,17l-25,17z"/>
|
android:pathData="M145,70l30,20l-30,20z"/>
|
||||||
</group>
|
|
||||||
|
|
||||||
<!-- App Name -->
|
<!-- tvmon Text - White -->
|
||||||
|
<!-- Letter 't' -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M120,130h80v4h-80z"/>
|
android:pathData="M100,148h6v-3h-6v-4h10v18h-4v-11h-6z"/>
|
||||||
|
<!-- Letter 'v' -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M140,136h40v3h-40z"/>
|
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>
|
</vector>
|
||||||
@@ -5,12 +5,7 @@
|
|||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
|
||||||
<!-- Background Circle -->
|
<!-- TV Screen with white color -->
|
||||||
<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
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M34,38h40v28h-40z"/>
|
android:pathData="M34,38h40v28h-40z"/>
|
||||||
@@ -23,12 +18,12 @@
|
|||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M40,74h28v2h-28z"/>
|
android:pathData="M40,74h28v2h-28z"/>
|
||||||
|
|
||||||
<!-- Play Button -->
|
<!-- Play Button - using Netflix red -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF6B6B"
|
android:fillColor="#E50914"
|
||||||
android:pathData="M50,44l12,8l-12,8z"/>
|
android:pathData="M50,44l12,8l-12,8z"/>
|
||||||
|
|
||||||
<!-- tvmon Text -->
|
<!-- tvmon Text - white -->
|
||||||
<!-- Letter 't' -->
|
<!-- Letter 't' -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
|
|||||||
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- 설정 버튼 카테고리 하단 배치"
|
||||||
|
}
|
||||||