Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

테마 마켓 추가 #383

Merged
merged 37 commits into from
Feb 22, 2025
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c01338d
ThemeDto의 필드를 nullable로 수정
eastshine2741 Oct 20, 2024
5b26aaf
GtCurrentTableThemeUseCase 생성
eastshine2741 Oct 20, 2024
812b80a
ThemeDetailViewModel init {} 함수분리
eastshine2741 Oct 20, 2024
db22d6c
ThemeDetailViewModel.isNewTheme의 backing field 제거
eastshine2741 Oct 20, 2024
8e1b8e3
null check용 depth 제거
eastshine2741 Oct 20, 2024
e073af3
시간표 색상 새로고침 조건문 단순화
eastshine2741 Oct 20, 2024
c337ea9
도메인모델 v2 추가
eastshine2741 Oct 20, 2024
2247639
비즈니스 로직 리팩토링
eastshine2741 Nov 16, 2024
c77b904
v2 도메인 모델으로 대체
eastshine2741 Nov 16, 2024
36331b7
ThemeDetailPage 리팩토링
eastshine2741 Nov 16, 2024
48a75d0
UiState Error, Loading 대응
eastshine2741 Nov 16, 2024
c7ad540
isEditable 조건 오타 수정 & SNUTT 외 테마 색상 옮겨오기
eastshine2741 Nov 16, 2024
b23ad10
ThemeColorRow 애니메이션 버그 수정
eastshine2741 Nov 16, 2024
71dd5b6
isEditable 조건 오타 수정 2
eastshine2741 Nov 16, 2024
09f76cc
누락된 private 추가
eastshine2741 Nov 16, 2024
fd29300
ThemeDetail Preview 몇 개 추가
eastshine2741 Nov 16, 2024
4540741
ThemeListViewModel 로직 개선
eastshine2741 Nov 16, 2024
405cf21
ThemeConfigScreen 리팩토링
eastshine2741 Nov 16, 2024
2f91a1a
ThemeListViewModel -> ThemeConfigViewModel 이름 수정
eastshine2741 Nov 16, 2024
1129f1d
누락된 private 추가
eastshine2741 Nov 16, 2024
05a8a5e
ThemeConfig Preview 몇 개 추가
eastshine2741 Nov 16, 2024
ee00ec4
lint
eastshine2741 Nov 16, 2024
4a8a70b
ThemeConfigViewModel의 StateFlow가 backing field 가지도록 수정
eastshine2741 Nov 16, 2024
186e59b
네이밍 수정
eastshine2741 Nov 16, 2024
0a2fb22
담은 테마 따로 보여주기
eastshine2741 Nov 16, 2024
95f7019
더보기 탭 테마마켓 진입점 생성
eastshine2741 Sep 15, 2024
3b8730f
ThemeMarketScreen 추가
eastshine2741 Sep 16, 2024
328bf87
테마마켓 웹뷰 추가
eastshine2741 Dec 14, 2024
eb42341
담은 테마가 없으면 담은 테마 UI 보여주지 않도록 수정
eastshine2741 Dec 15, 2024
b4a27dd
lint
eastshine2741 Dec 15, 2024
3918fed
웹뷰 Loading, Error 대응
eastshine2741 Dec 15, 2024
92587c5
테마마켓용 string resource 생성
eastshine2741 Dec 15, 2024
e33312b
LoadState.InitialLoading 삭제
eastshine2741 Dec 15, 2024
078d637
Merge branch 'develop' into eastshine2741/add-custom-theme-market
eastshine2741 Jan 27, 2025
186a994
웹뷰 영역이 전체를 채우도록 수정
eastshine2741 Jan 27, 2025
65a4181
담은 테마에 3dot 아이콘, 바텀시트 추가
eastshine2741 Jan 28, 2025
d7dec71
Merge branch 'develop' into eastshine2741/add-custom-theme-market
eastshine2741 Feb 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class ReviewWebViewContainer(
private val accessToken: StateFlow<String?>,
private val isDarkMode: Boolean,
) : WebViewContainer {
val loadState: MutableState<LoadState> = mutableStateOf(LoadState.InitialLoading(0))
val loadState: MutableState<LoadState> = mutableStateOf(LoadState.Loading(0))

override val webView: WebView = WebView(context).apply {
if (BuildConfig.DEBUG) {
Expand All @@ -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(
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String?>,
private val isDarkMode: Boolean,
) : WebViewContainer {
val loadState: MutableState<LoadState> = 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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import android.webkit.WebView
interface WebViewContainer {
val webView: WebView

suspend fun openPage(url: String?)
suspend fun openPage(url: String? = null)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ data class ThemeDto(
val name: String?,
val colors: List<ColorDto>?,
val isCustom: Boolean?,
val status: String?,
) {

fun toTableTheme(): TableTheme {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.NotificationPage
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.*
Expand Down Expand Up @@ -431,6 +432,11 @@ class RootActivity : AppCompatActivity() {
val vacancyViewModel = hiltViewModel<VacancyViewModel>(parentEntry)
VacancyPage(vacancyViewModel)
}
composable2(NavigationDestination.ThemeMarket) {
ThemeMarketRoute(
onBackClick = { navController.popBackStack() },
)
}
composable2(NavigationDestination.ThemeConfig) {
ThemeConfigRoute(
onNavigateBack = { navController.popBackStack() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -62,3 +62,38 @@ fun CustomThemeMoreActionBottomSheet(
)
}
}

@Composable
fun MarketCustomThemeMoreActionBottomSheet(
onClickDetail: () -> Unit,
onClickDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
Comment on lines +66 to +71
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흐음 혹시 이 MarketCustomThemeMoreActionBottomSheetMyCustomThemeMoreActionBottomSheet 잘 합치는건 별로인가..?
굳이 무리한 합치기이면 필요 없긴 한데 약간 중복 느낌도 있어서
+) 으음 전체적으로 따로따로 있구나 굳이 합칠필요 없는것 같기도

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마자 공용 함수로 합치면 MarketCustomTheme에는 필요없는 콜백을 MarketCustomTheme이 받게 되는 게 싫어서 분리했어

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() },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -109,7 +110,8 @@ fun ThemeConfigRoute(
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ThemeConfigScreen(
customThemes: List<CustomTheme>,
myCustomThemes: List<CustomTheme>,
marketCustomThemes: List<CustomTheme>,
builtInThemes: List<BuiltInTheme>,
onNavigateBack: () -> Unit,
onFetchThemes: suspend () -> Unit,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,7 +20,13 @@ class ThemeConfigViewModel @Inject constructor(
currentTableRepository: CurrentTableRepository,
) : ViewModel() {

val customThemes: StateFlow<List<CustomTheme>> = 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<List<BuiltInTheme>> = themeRepository.builtInThemes

val currentTable = currentTableRepository.currentTable
Expand Down
Loading
Loading