diff --git a/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/LoadState.kt b/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/LoadState.kt index b725595d1..181f10341 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/LoadState.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/LoadState.kt @@ -1,8 +1,7 @@ package com.wafflestudio.snutt2.lib.android.webview sealed class LoadState { - object Success : LoadState() - object Error : LoadState() + data object Success : LoadState() + data object Error : LoadState() data class Loading(val progress: Int) : LoadState() - data class InitialLoading(val progress: Int) : LoadState() } diff --git a/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/ReviewWebViewContainer.kt b/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/ReviewWebViewContainer.kt index 2a721e143..d6ad64204 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/ReviewWebViewContainer.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/ReviewWebViewContainer.kt @@ -18,7 +18,7 @@ class ReviewWebViewContainer( private val accessToken: StateFlow, private val isDarkMode: Boolean, ) : WebViewContainer { - val loadState: MutableState = mutableStateOf(LoadState.InitialLoading(0)) + val loadState: MutableState = mutableStateOf(LoadState.Loading(0)) override val webView: WebView = WebView(context).apply { if (BuildConfig.DEBUG) { @@ -32,10 +32,7 @@ class ReviewWebViewContainer( } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - loadState.value = when (loadState.value) { - is LoadState.InitialLoading -> LoadState.InitialLoading(0) - else -> LoadState.Loading(0) - } + loadState.value = LoadState.Loading(0) } override fun onReceivedError( @@ -49,7 +46,6 @@ class ReviewWebViewContainer( this.webChromeClient = object : WebChromeClient() { override fun onProgressChanged(view: WebView?, newProgress: Int) { when (loadState.value) { - is LoadState.InitialLoading -> LoadState.InitialLoading(newProgress) is LoadState.Loading -> LoadState.Loading(newProgress) else -> null }?.let { diff --git a/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/ThemeMarketWebViewContainer.kt b/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/ThemeMarketWebViewContainer.kt new file mode 100644 index 000000000..20d99eabf --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/ThemeMarketWebViewContainer.kt @@ -0,0 +1,115 @@ +package com.wafflestudio.snutt2.lib.android.webview + +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import android.webkit.CookieManager +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import com.wafflestudio.snutt2.BuildConfig +import com.wafflestudio.snutt2.R +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import java.net.URL + +class ThemeMarketWebViewContainer( + private val context: Context, + private val accessToken: StateFlow, + private val isDarkMode: Boolean, +) : WebViewContainer { + val loadState: MutableState = mutableStateOf(LoadState.Loading(0)) + + override val webView: WebView = WebView(context).apply { + if (BuildConfig.DEBUG) { + WebView.setWebContentsDebuggingEnabled(true) + } + this.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + if (loadState.value != LoadState.Error) { + loadState.value = LoadState.Success + } + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + loadState.value = LoadState.Loading(0) + } + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError?, + ) { + loadState.value = LoadState.Error + } + } + this.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + when (loadState.value) { + is LoadState.Loading -> LoadState.Loading(newProgress) + else -> null + }?.let { + loadState.value = it + } + } + } + this.settings.javaScriptEnabled = true + } + + override suspend fun openPage(url: String?) { + val accessToken = accessToken.filterNotNull().first() + val themeMarketUrl = url ?: THEME_MARKET_URL + val urlHost = URL(themeMarketUrl).host + + setCookies(urlHost, accessToken) + webView.loadUrl(themeMarketUrl) + } + + private fun setCookies(host: String, accessToken: String) { + CookieManager.getInstance().apply { + setCookie( + host, + "x-access-apikey=${context.getString(R.string.api_key)}", + ) + setCookie( + host, + "x-access-token=$accessToken", + ) + setCookie( + host, + "x-os-type=android", + ) + setCookie( + host, + "x-os-version=${Build.VERSION.SDK_INT}", + ) + setCookie( + host, + "x-app-version=${BuildConfig.VERSION_NAME}", + ) + setCookie( + host, + "x-app-type=${if (BuildConfig.DEBUG) "debug" else "release"}", + ) + setCookie( + host, + "theme=${ + if (isDarkMode) { + "dark" + } else { + "light" + } + }", + ) + }.flush() + } + + companion object { + private val THEME_MARKET_URL = "https://snutt-theme-market-dev.wafflestudio.com/download" + } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/WebViewContainer.kt b/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/WebViewContainer.kt index c77d6fa55..f454d340b 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/WebViewContainer.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/WebViewContainer.kt @@ -5,5 +5,5 @@ import android.webkit.WebView interface WebViewContainer { val webView: WebView - suspend fun openPage(url: String?) + suspend fun openPage(url: String? = null) } diff --git a/app/src/main/java/com/wafflestudio/snutt2/lib/network/dto/core/ThemeDto.kt b/app/src/main/java/com/wafflestudio/snutt2/lib/network/dto/core/ThemeDto.kt index 896df16be..ceabcd93f 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/lib/network/dto/core/ThemeDto.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/lib/network/dto/core/ThemeDto.kt @@ -12,6 +12,7 @@ data class ThemeDto( val name: String?, val colors: List?, val isCustom: Boolean?, + val status: String?, ) { fun toTableTheme(): TableTheme { @@ -20,7 +21,7 @@ data class ThemeDto( id = id!!, name = name ?: "", colors = colors ?: emptyList(), - isFromMarket = false, // FIXME: 서버 응답에 맞게 수정 + isFromMarket = status == "DOWNLOADED", ) } else { BuiltInTheme.fromCode(theme ?: 0) diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/NavigationDestination.kt b/app/src/main/java/com/wafflestudio/snutt2/views/NavigationDestination.kt index 7aa9eeea1..043f28518 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/NavigationDestination.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/NavigationDestination.kt @@ -27,6 +27,7 @@ object NavigationDestination { const val Bookmark = "bookmarks" const val NetworkLog = "network_log" const val VacancyNotification = "vacancy" + const val ThemeMarket = "theme_market" const val Friends = "friends" const val ThemeConfig = "theme_config" const val ThemeDetail = "theme_detail" diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/RootActivity.kt b/app/src/main/java/com/wafflestudio/snutt2/views/RootActivity.kt index 352d66294..387f8708a 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/RootActivity.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/RootActivity.kt @@ -74,6 +74,7 @@ import com.wafflestudio.snutt2.views.logged_in.lecture_detail.LectureDetailViewM import com.wafflestudio.snutt2.views.logged_in.lecture_detail.deeplink.TimetableLectureDetailPage import com.wafflestudio.snutt2.views.logged_in.notifications.NotificationRoute import com.wafflestudio.snutt2.views.logged_in.table_lectures.LecturesOfTablePage +import com.wafflestudio.snutt2.views.logged_in.thememarket.ThemeMarketRoute import com.wafflestudio.snutt2.views.logged_in.vacancy_noti.VacancyPage import com.wafflestudio.snutt2.views.logged_in.vacancy_noti.VacancyViewModel import com.wafflestudio.snutt2.views.logged_out.* @@ -431,6 +432,11 @@ class RootActivity : AppCompatActivity() { val vacancyViewModel = hiltViewModel(parentEntry) VacancyPage(vacancyViewModel) } + composable2(NavigationDestination.ThemeMarket) { + ThemeMarketRoute( + onBackClick = { navController.popBackStack() }, + ) + } composable2(NavigationDestination.ThemeConfig) { ThemeConfigRoute( onNavigateBack = { navController.popBackStack() }, diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/reviews/ReviewPage.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/reviews/ReviewPage.kt index 05be9b348..8f47b0233 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/reviews/ReviewPage.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/reviews/ReviewPage.kt @@ -76,11 +76,6 @@ fun ReviewWebView(height: Float = 1.0f) { onRetry = { scope.launch { webViewContainer.reload() } }, ) - is LoadState.InitialLoading -> WebViewLoading( - modifier = Modifier.fillMaxSize(), - progress = loadState.progress / 100.0f, - ) - is LoadState.Loading -> WebViewLoading( modifier = Modifier.fillMaxSize(), progress = loadState.progress / 100.0f, diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/SettingsPage.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/SettingsPage.kt index d07a75786..7a1b9403d 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/SettingsPage.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/SettingsPage.kt @@ -135,6 +135,15 @@ fun SettingsPage( ) }, ) + SettingItem( + title = stringResource(R.string.settings_item_theme_market), + hasNextPage = true, + onClick = { + navController.navigate( + NavigationDestination.ThemeMarket, + ) + }, + ) } Margin(height = 10.dp) SettingColumn { diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/CustomThemeMoreActionBottomSheet.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/CustomThemeMoreActionBottomSheet.kt index 7297b226d..4f286eea5 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/CustomThemeMoreActionBottomSheet.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/CustomThemeMoreActionBottomSheet.kt @@ -18,7 +18,7 @@ import com.wafflestudio.snutt2.components.compose.PaletteIcon import com.wafflestudio.snutt2.components.compose.TrashIcon @Composable -fun CustomThemeMoreActionBottomSheet( +fun MyCustomThemeMoreActionBottomSheet( onClickDetail: () -> Unit, onClickDuplicate: () -> Unit, onClickDelete: () -> Unit, @@ -62,3 +62,38 @@ fun CustomThemeMoreActionBottomSheet( ) } } + +@Composable +fun MarketCustomThemeMoreActionBottomSheet( + onClickDetail: () -> Unit, + onClickDelete: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(MaterialTheme.colors.surface) + .padding(vertical = 12.dp) + .fillMaxWidth(), + ) { + MoreActionItem( + icon = { + PaletteIcon( + modifier = Modifier.size(30.dp), + colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), + ) + }, + text = stringResource(R.string.custom_theme_action_detail_view), + onClick = { onClickDetail() }, + ) + MoreActionItem( + icon = { + TrashIcon( + modifier = Modifier.size(30.dp), + colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), + ) + }, + text = stringResource(R.string.custom_theme_action_delete), + onClick = { onClickDelete() }, + ) + } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/ThemeConfigScreen.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/ThemeConfigScreen.kt index 986cb08b0..03d575dd6 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/ThemeConfigScreen.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/ThemeConfigScreen.kt @@ -78,12 +78,13 @@ fun ThemeConfigRoute( ) { val apiOnError = LocalApiOnError.current val apiOnProgress = LocalApiOnProgress.current - - val customThemes by themeConfigViewModel.customThemes.collectAsState() + val myCustomThemes by themeConfigViewModel.myCustomThemes.collectAsState() + val marketCustomThemes by themeConfigViewModel.marketCustomThemes.collectAsState() val builtInThemes by themeConfigViewModel.builtInThemes.collectAsState() ThemeConfigScreen( - customThemes = customThemes, + myCustomThemes = myCustomThemes, + marketCustomThemes = marketCustomThemes, builtInThemes = builtInThemes, onNavigateBack = onNavigateBack, onFetchThemes = { @@ -109,7 +110,8 @@ fun ThemeConfigRoute( @OptIn(ExperimentalMaterialApi::class) @Composable fun ThemeConfigScreen( - customThemes: List, + myCustomThemes: List, + marketCustomThemes: List, builtInThemes: List, onNavigateBack: () -> Unit, onFetchThemes: suspend () -> Unit, @@ -162,12 +164,12 @@ fun ThemeConfigScreen( ) { ThemesRow( title = stringResource(R.string.theme_config_custom_theme), - themes = customThemes, + themes = myCustomThemes, onClickItem = onNavigateToDetail, onClickMore = { theme -> scope.launch { bottomSheet.setSheetContent { - CustomThemeMoreActionBottomSheet( + MyCustomThemeMoreActionBottomSheet( onClickDetail = { scope.launch { onNavigateToDetail(theme) @@ -201,6 +203,39 @@ fun ThemeConfigScreen( ) }, ) + if (marketCustomThemes.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + ThemesRow( + title = stringResource(R.string.theme_config_market_custom_theme), + themes = marketCustomThemes, + onClickItem = onNavigateToDetail, + onClickMore = { theme -> + scope.launch { + bottomSheet.setSheetContent { + MarketCustomThemeMoreActionBottomSheet( + onClickDetail = { + scope.launch { + onNavigateToDetail(theme) + bottomSheet.hide() + } + }, + onClickDelete = { + showDeleteThemeDialog( + composableStates = composableStates, + onConfirm = { + onDeleteTheme(theme) + modalState.hide() + bottomSheet.hide() + }, + ) + }, + ) + } + bottomSheet.show() + } + }, + ) + } Spacer(modifier = Modifier.height(4.dp)) ThemesRow( title = stringResource(R.string.theme_config_builtin_theme), diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/ThemeConfigViewModel.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/ThemeConfigViewModel.kt index 1f05872be..ce9da82e6 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/ThemeConfigViewModel.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/theme/ThemeConfigViewModel.kt @@ -1,9 +1,11 @@ package com.wafflestudio.snutt2.views.logged_in.home.settings.theme import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.wafflestudio.snutt2.data.current_table.CurrentTableRepository import com.wafflestudio.snutt2.data.tables.TableRepository import com.wafflestudio.snutt2.data.themes.ThemeRepository +import com.wafflestudio.snutt2.lib.map import com.wafflestudio.snutt2.model.BuiltInTheme import com.wafflestudio.snutt2.model.CustomTheme import com.wafflestudio.snutt2.model.TableTheme @@ -18,7 +20,13 @@ class ThemeConfigViewModel @Inject constructor( currentTableRepository: CurrentTableRepository, ) : ViewModel() { - val customThemes: StateFlow> = themeRepository.customThemes + val customThemes = themeRepository.customThemes + val myCustomThemes = themeRepository.customThemes.map(viewModelScope) { customThemes -> + customThemes.filter { it.isFromMarket.not() } + } + val marketCustomThemes = themeRepository.customThemes.map(viewModelScope) { customThemes -> + customThemes.filter { it.isFromMarket } + } val builtInThemes: StateFlow> = themeRepository.builtInThemes val currentTable = currentTableRepository.currentTable diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/thememarket/ThemeMarketScreen.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/thememarket/ThemeMarketScreen.kt new file mode 100644 index 000000000..306174c50 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/thememarket/ThemeMarketScreen.kt @@ -0,0 +1,72 @@ +package com.wafflestudio.snutt2.views.logged_in.thememarket + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.wafflestudio.snutt2.R +import com.wafflestudio.snutt2.components.compose.SimpleTopBar +import com.wafflestudio.snutt2.lib.android.webview.ThemeMarketWebViewContainer +import com.wafflestudio.snutt2.ui.isDarkMode +import com.wafflestudio.snutt2.views.logged_in.home.settings.UserViewModel +import kotlinx.coroutines.flow.StateFlow + +@Composable +fun ThemeMarketRoute( + onBackClick: () -> Unit, + userViewModel: UserViewModel = hiltViewModel(), +) { + ThemeMarketScreen( + accessToken = userViewModel.accessToken, + onBackClick = onBackClick, + modifier = Modifier.fillMaxSize(), + ) +} + +@Composable +fun ThemeMarketScreen( + accessToken: StateFlow, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val isDarkMode = isDarkMode() + val webViewContainer = remember { + ThemeMarketWebViewContainer( + context = context, + accessToken = accessToken, + isDarkMode = isDarkMode, + ) + } + + BackHandler { + if (webViewContainer.webView.canGoBack()) { + webViewContainer.webView.goBack() + } else { + onBackClick() + } + } + + LaunchedEffect(Unit) { + webViewContainer.openPage() + } + + Column( + modifier = modifier, + ) { + SimpleTopBar( + title = stringResource(R.string.theme_market_app_bar_title), + onClickNavigateBack = onBackClick, + ) + ThemeMarketWebView( + themeMarketWebViewContainer = webViewContainer, + modifier = Modifier.fillMaxSize(), + ) + } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/thememarket/ThemeMarketWebView.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/thememarket/ThemeMarketWebView.kt new file mode 100644 index 000000000..ba38f88b9 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/thememarket/ThemeMarketWebView.kt @@ -0,0 +1,149 @@ +package com.wafflestudio.snutt2.views.logged_in.thememarket + +import android.webkit.WebView +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.wafflestudio.snutt2.R +import com.wafflestudio.snutt2.lib.android.webview.LoadState +import com.wafflestudio.snutt2.lib.android.webview.ThemeMarketWebViewContainer +import com.wafflestudio.snutt2.ui.SNUTTColors +import com.wafflestudio.snutt2.ui.SNUTTTheme +import com.wafflestudio.snutt2.ui.SNUTTTypography +import kotlinx.coroutines.launch + +@Composable +fun ThemeMarketWebView( + themeMarketWebViewContainer: ThemeMarketWebViewContainer, + modifier: Modifier = Modifier, +) { + val scope = rememberCoroutineScope() + when (val loadState = themeMarketWebViewContainer.loadState.value) { + LoadState.Error -> ThemeMarketWebViewError( + onRetry = { + scope.launch { + themeMarketWebViewContainer.openPage() + } + }, + modifier = modifier, + ) + is LoadState.Loading -> ThemeMarketWebViewLoading( + progress = loadState.progress / 100f, + modifier = modifier, + ) + LoadState.Success -> ThemeMarketWebViewSuccess( + webView = themeMarketWebViewContainer.webView, + modifier = modifier, + ) + } +} + +@Composable +private fun ThemeMarketWebViewError( + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.size(width = 50.dp, height = 58.dp), + painter = painterResource(R.drawable.ic_cat_retry), + contentDescription = "네트워크 연결을 확인해주세요.", + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = stringResource(R.string.theme_market_webview_error), + style = SNUTTTypography.subtitle1, + color = SNUTTColors.Black900, + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Button( + onClick = onRetry, + colors = ButtonDefaults.buttonColors(backgroundColor = SNUTTColors.Sky), + ) { + Text( + text = stringResource(id = R.string.theme_market_webview_error_retry), + style = SNUTTTypography.h3, + color = SNUTTColors.White900, + ) + } + } +} + +@Composable +private fun ThemeMarketWebViewLoading( + progress: Float, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Top, + ) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(2.dp), + progress = progress, + color = SNUTTColors.Gray200, + ) + } +} + +@Composable +private fun ThemeMarketWebViewSuccess( + webView: WebView, + modifier: Modifier = Modifier, +) { + AndroidView( + factory = { + webView + }, + modifier = modifier.clipToBounds(), // Compose에서 WebView 사용 시, WebView가 잠깐 동안 다른 Composable을 가리는 WebView 버그 대응(https://issuetracker.google.com/issues/174233728?pli=1#comment5) + ) +} + +@Preview(showBackground = true, heightDp = 640) +@Composable +private fun ThemeMarketWebViewErrorPreview() { + SNUTTTheme { + ThemeMarketWebViewError( + onRetry = {}, + ) + } +} + +@Preview(showBackground = true, heightDp = 640) +@Composable +private fun ThemeMarketWebViewLoadingPreview() { + SNUTTTheme { + ThemeMarketWebViewLoading( + progress = 0.5f, + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a82d95114..cf021b67d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -425,6 +425,7 @@ 불편한 점이나 버그를 제보해주세요.\n더 나은 SNUTT를 위한 아이디어도 환영해요. 개인정보처리방침 빈자리 알림 + 테마 마켓 서비스 약관 버전 정보 개발자 정보 @@ -551,6 +552,7 @@ 여기를 눌러 빈자리 알림 서비스를 이용해 보세요! 시간표 테마 커스텀 테마 + 담은 테마 제공 테마 테마는 어떻게 적용하나요? 시간표 목록 > 더보기 버튼 > 테마 설정 @@ -600,4 +602,7 @@ 카카오톡 초대 기능을 사용할 수 없습니다. 알 수 없는 오류가 발생했습니다. 카카오톡이 설치되어 있는지 확인해보세요. + 테마 마켓 + 네트워크 연결을 확인해주세요 + 다시 불러오기