diff --git a/app/src/main/java/com/kuit/afternote/data/remote/AuthInterceptor.kt b/app/src/main/java/com/kuit/afternote/data/remote/AuthInterceptor.kt index 4e29748c..c682578b 100644 --- a/app/src/main/java/com/kuit/afternote/data/remote/AuthInterceptor.kt +++ b/app/src/main/java/com/kuit/afternote/data/remote/AuthInterceptor.kt @@ -32,6 +32,12 @@ class AuthInterceptor private const val BASE_URL = "https://afternote.kro.kr/" private const val REISSUE_ENDPOINT = "auth/reissue" + /** + * 토큰 재발급 동기화 락. + * companion object에 두어 프로세스 전역에서 하나의 refresh만 실행되도록 보장. + */ + private val refreshTokenLock = Any() + /** * 인증이 필요 없는 경로 목록. */ @@ -107,10 +113,10 @@ class AuthInterceptor val response = proceedAndLog(chain, authenticatedRequest) - // 401 응답 시 토큰 재발급 시도 + // 401 응답 시 토큰 재발급 시도 (synchronized double-check locking) if (response.code == 401) { Log.w(TAG, "Auth: 401 Unauthorized - Attempting token refresh") - return handleTokenRefresh(chain, originalRequest, response) + return handleTokenRefresh(chain, originalRequest, response, accessToken) } return response @@ -118,59 +124,74 @@ class AuthInterceptor /** * 401 응답 시 토큰 재발급을 시도하고 요청을 재시도합니다. + * + * synchronized + double-check locking으로 동시 401에서 reissue API가 + * 한 번만 호출되도록 보장합니다. 나머지 스레드는 lock을 기다린 후 + * 이미 갱신된 토큰으로 바로 재시도합니다. + * + * @param failedAccessToken 401을 받은 시점의 accessToken (double-check에 사용) */ private fun handleTokenRefresh( chain: Interceptor.Chain, originalRequest: Request, - originalResponse: Response + originalResponse: Response, + failedAccessToken: String, ): Response { - val refreshToken = runBlocking { tokenManager.getRefreshToken() } + // refreshTokenLock으로 동기화 — 동시에 하나의 스레드만 refresh 수행 + synchronized(refreshTokenLock) { + // Double-check: lock을 기다리는 동안 다른 스레드가 이미 갱신했는지 확인 + val currentAccessToken = runBlocking { tokenManager.getAccessToken() } + if (currentAccessToken != failedAccessToken && !currentAccessToken.isNullOrEmpty()) { + // 다른 스레드가 이미 토큰을 갱신함 → refresh 없이 새 토큰으로 재시도 + Log.d(TAG, "TokenRefresh: Already refreshed by another thread, retrying") + originalResponse.close() + val newRequest = originalRequest + .newBuilder() + .header(AUTHORIZATION_HEADER, "$BEARER_PREFIX$currentAccessToken") + .build() + return proceedAndLog(chain, newRequest) + } - if (refreshToken.isNullOrEmpty()) { - Log.e(TAG, "TokenRefresh: No refresh token available") - // 토큰이 전혀 없는 상태에서는 추가 조치를 하지 않고 401을 그대로 반환한다. - // (UI 레이어에서 isLoggedInFlow를 보고 로그인 화면으로 유도) - return originalResponse - } + // 이 스레드가 최초 진입 → 실제 refresh 수행 + val refreshToken = runBlocking { tokenManager.getRefreshToken() } + + if (refreshToken.isNullOrEmpty()) { + Log.e(TAG, "TokenRefresh: No refresh token available") + return originalResponse + } - Log.d(TAG, "TokenRefresh: Attempting with refreshToken=${refreshToken.take(n = 20)}...") + Log.d(TAG, "TokenRefresh: Attempting with refreshToken=${refreshToken.take(n = 20)}...") - return try { - val newTokens = refreshAccessToken(refreshToken) + return try { + val newTokens = refreshAccessToken(refreshToken) - if (newTokens != null) { - Log.d(TAG, "TokenRefresh: SUCCESS - New tokens received") + if (newTokens != null) { + Log.d(TAG, "TokenRefresh: SUCCESS - New tokens received") - // 새 토큰 저장 - runBlocking { - tokenManager.updateTokens( - accessToken = newTokens.accessToken ?: "", - refreshToken = newTokens.refreshToken ?: refreshToken - ) - } + runBlocking { + tokenManager.updateTokens( + accessToken = newTokens.accessToken ?: "", + refreshToken = newTokens.refreshToken ?: refreshToken + ) + } - // 원래 응답 닫기 - originalResponse.close() + originalResponse.close() - // 새 토큰으로 원래 요청 재시도 - val newRequest = originalRequest - .newBuilder() - .header(AUTHORIZATION_HEADER, "$BEARER_PREFIX${newTokens.accessToken}") - .build() + val newRequest = originalRequest + .newBuilder() + .header(AUTHORIZATION_HEADER, "$BEARER_PREFIX${newTokens.accessToken}") + .build() - Log.d(TAG, "TokenRefresh: Retrying original request with new token") - proceedAndLog(chain, newRequest) - } else { - Log.e(TAG, "TokenRefresh: FAILED - keeping existing tokens") - // 재발급 실패 시 기존 토큰을 삭제하지 않고 401을 그대로 반환한다. - // 이렇게 해야 사용자가 이미 로그인했다고 생각하는 상태에서 - // 토큰이 갑자기 null로 사라지는 문제를 방지할 수 있다. + Log.d(TAG, "TokenRefresh: Retrying original request with new token") + proceedAndLog(chain, newRequest) + } else { + Log.e(TAG, "TokenRefresh: FAILED - keeping existing tokens") + originalResponse + } + } catch (e: Exception) { + Log.e(TAG, "TokenRefresh: Exception - ${e.message}", e) originalResponse } - } catch (e: Exception) { - Log.e(TAG, "TokenRefresh: Exception - ${e.message}", e) - // 예외 발생 시에도 토큰을 강제로 삭제하지 않고 401 응답을 그대로 반환한다. - originalResponse } } diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/upload/FuneralVideoUpload.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/upload/FuneralVideoUpload.kt index d5448c3d..e757d373 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/upload/FuneralVideoUpload.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/upload/FuneralVideoUpload.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -41,6 +42,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.net.toUri import coil3.compose.AsyncImage +import coil3.compose.AsyncImagePainter import coil3.network.NetworkHeaders import coil3.network.httpHeaders import coil3.request.ImageRequest @@ -82,6 +84,15 @@ fun FuneralVideoUpload( val context = LocalContext.current var thumbnailBitmap by remember(videoUrl) { mutableStateOf(null) } + LaunchedEffect(videoUrl, thumbnailUrl, hasVideo, thumbnailBitmap) { + Log.d( + TAG, + "FuneralVideoUpload active: videoUrl=${videoUrl?.take(50) ?: "null"}, " + + "thumbnailUrl=${thumbnailUrl?.take(80) ?: "null"}, hasVideo=$hasVideo, " + + "hasBitmap=${thumbnailBitmap != null}" + ) + } + LaunchedEffect(videoUrl) { if (videoUrl.isNullOrBlank()) { thumbnailBitmap = null @@ -175,6 +186,9 @@ fun FuneralVideoUpload( ) { when { !thumbnailUrl.isNullOrBlank() -> { + SideEffect { + Log.d(TAG, "Branch=URL, thumbnailUrl=$thumbnailUrl") + } AsyncImage( model = ImageRequest.Builder(context) @@ -188,17 +202,26 @@ fun FuneralVideoUpload( contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, - error = painterResource(R.drawable.ic_add_circle), - onError = { state -> + error = painterResource(R.drawable.img_placeholder_1), + onLoading = { + Log.d(TAG, "Coil loading started: url=$thumbnailUrl") + }, + onSuccess = { + Log.d(TAG, "Coil load success: url=$thumbnailUrl") + }, + onError = { state: AsyncImagePainter.State.Error -> Log.e( TAG, - "Thumbnail load failed: url=$thumbnailUrl", + "Coil load failed: url=$thumbnailUrl", state.result.throwable ) } ) } thumbnailBitmap != null -> { + SideEffect { + Log.d(TAG, "Branch=LocalBitmap, videoUrl=$videoUrl") + } Image( bitmap = thumbnailBitmap!!, contentDescription = null, @@ -206,6 +229,15 @@ fun FuneralVideoUpload( contentScale = ContentScale.Crop ) } + else -> { + SideEffect { + Log.w( + TAG, + "Branch=NoThumbnail, videoUrl=$videoUrl, " + + "thumbnailUrl=$thumbnailUrl, thumbnailBitmap=null" + ) + } + } } Image( painter = painterResource(R.drawable.ic_playback), diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordMainScreen.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordMainScreen.kt index 0a647e3e..c43e8695 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordMainScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordMainScreen.kt @@ -5,13 +5,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape @@ -59,6 +56,9 @@ fun RecordMainScreen( Scaffold( modifier = modifier.fillMaxSize(), containerColor = Gray1, + topBar = { + TopBar(title = "나의 모든 기록") + }, bottomBar = { BottomNavigationBar( selectedItem = selectedBottomNavItem, @@ -103,12 +103,7 @@ fun RecordMainScreen( modifier = Modifier .fillMaxWidth() .padding(paddingValues) - .windowInsetsPadding(WindowInsets.statusBars) // 상태바 만큼 패딩을 줘서 겹치지 않도록 ) { - // 샹단 제목 - TopBar( - title = "나의 모든 기록" - ) // 리스트 val allItems = listOf( "데일리 질문답변" to "매일 다른 질문들에 나를 남겨보세요", diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordViewModel.kt index 0bd6d6a9..89473cfa 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordViewModel.kt @@ -5,16 +5,26 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kuit.afternote.feature.afternote.domain.model.AfternoteItem import com.kuit.afternote.feature.afternote.domain.usecase.GetAfternotesUseCase -import com.kuit.afternote.feature.dailyrecord.data.dto.Emotion import com.kuit.afternote.feature.dailyrecord.data.dto.EmotionResponse import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordRequest -import com.kuit.afternote.feature.dailyrecord.domain.usecase.* +import com.kuit.afternote.feature.dailyrecord.domain.usecase.CreateMindRecordUseCase +import com.kuit.afternote.feature.dailyrecord.domain.usecase.DeleteMindRecordUseCase +import com.kuit.afternote.feature.dailyrecord.domain.usecase.EditMindRecordUseCase +import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetDailyQuestionUseCase +import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetEmotionsUseCase +import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetMindRecordUseCase +import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetMindRecordsUseCase import com.kuit.afternote.feature.dailyrecord.presentation.uimodel.MindRecordUiModel import com.kuit.afternote.feature.dailyrecord.presentation.uimodel.MindRecordUiState import com.kuit.afternote.feature.home.presentation.component.CalendarDay import com.kuit.afternote.feature.home.presentation.component.CalendarDayStyle import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import retrofit2.HttpException import java.time.DayOfWeek @@ -48,6 +58,14 @@ data class EditRecordParams( val isDraft: Boolean ) +/** + * Contract for HomeScreen so a fake can be used in Previews (no Hilt). + */ +interface MindRecordHomeContract { + val calendarDays: StateFlow> + fun loadRecordsForDiaryList() +} + @HiltViewModel class MindRecordViewModel @Inject constructor( private val getMindRecordsUseCase: GetMindRecordsUseCase, @@ -58,7 +76,7 @@ class MindRecordViewModel @Inject constructor( private val editMindRecordUseCase: EditMindRecordUseCase, private val getAfternotesUseCase: GetAfternotesUseCase, private val getEmotionsUseCase: GetEmotionsUseCase -) : ViewModel() { +) : ViewModel(), MindRecordHomeContract { // --- State 정의 --- private val _records = MutableStateFlow>(emptyList()) @@ -124,7 +142,7 @@ class MindRecordViewModel @Inject constructor( .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), WeeklySummaryUiState()) // 기존 캘린더 데이 (메인 화면용 유지) - val calendarDays: StateFlow> = _records + override val calendarDays: StateFlow> = _records .map { recordList -> val recordedDates = recordList.map { it.originalDate }.toSet() val today = LocalDate.now() @@ -215,7 +233,7 @@ class MindRecordViewModel @Inject constructor( MindRecordUiModel( id = summary.recordId, title = summary.title ?: "", - formattedDate = runCatching { formatDate(summary.date) }.getOrElse { summary.date }, + formattedDate = runCatching { formatDate(summary.date) }.getOrNull() ?: summary.date, draftLabel = if (summary.isDraft) "임시저장" else "완료", content = summary.content, originalDate = summary.date, @@ -239,7 +257,7 @@ class MindRecordViewModel @Inject constructor( } } - fun loadRecordsForDiaryList() { + override fun loadRecordsForDiaryList() { viewModelScope.launch { try { val diaryResult = getMindRecordsUseCase("DIARY", "LIST", null, null) @@ -251,7 +269,7 @@ class MindRecordViewModel @Inject constructor( MindRecordUiModel( id = summary.recordId, title = summary.title ?: "", - formattedDate = runCatching { formatDate(summary.date) }.getOrElse { summary.date }, + formattedDate = runCatching { formatDate(summary.date) }.getOrNull() ?: summary.date, draftLabel = if (summary.isDraft) "임시저장" else "완료", content = summary.content, originalDate = summary.date, @@ -358,7 +376,7 @@ class MindRecordViewModel @Inject constructor( _selectedRecord.value = MindRecordUiModel( id = detail.recordId, title = detail.title, - formattedDate = runCatching { formatDate(detail.date) }.getOrElse { detail.date }, + formattedDate = runCatching { formatDate(detail.date) }.getOrNull() ?: detail.date, draftLabel = if (detail.isDraft) "임시저장" else "완료", content = detail.content, type = detail.type, @@ -396,9 +414,7 @@ fun AfternoteItem.toMindRecordUiModel(): MindRecordUiModel { val parsedDate = runCatching { java.time.LocalDate.parse(this.date, inputFormatter) - }.getOrElse { - java.time.LocalDate.now() // 파싱 실패 시 오늘 날짜로 폴백 (시스템 안정성) - } + }.getOrNull() ?: java.time.LocalDate.now() // 파싱 실패 시 오늘 날짜로 폴백 (시스템 안정성) val originalDateStr = parsedDate.format(isoFormatter) // "2026-02-06" val formattedDateStr = "${parsedDate.monthValue}월 ${parsedDate.dayOfMonth}일" diff --git a/app/src/main/java/com/kuit/afternote/feature/home/presentation/component/HomeHeader.kt b/app/src/main/java/com/kuit/afternote/feature/home/presentation/component/HomeHeader.kt index 0cb26d4e..c40af064 100644 --- a/app/src/main/java/com/kuit/afternote/feature/home/presentation/component/HomeHeader.kt +++ b/app/src/main/java/com/kuit/afternote/feature/home/presentation/component/HomeHeader.kt @@ -25,7 +25,6 @@ import com.kuit.afternote.ui.theme.Gray9 @Composable fun HomeHeader( modifier: Modifier = Modifier, - onProfileClick: () -> Unit = {}, onSettingsClick: () -> Unit = {} ) { Row( @@ -37,7 +36,7 @@ fun HomeHeader( verticalAlignment = Alignment.CenterVertically ) { Image( - painter = painterResource(R.drawable.logo_blue), + painter = painterResource(R.drawable.ic_home_topbar_logo), contentDescription = "AFTERNOTE", modifier = Modifier.height(16.dp) ) diff --git a/app/src/main/java/com/kuit/afternote/feature/home/presentation/screen/HomeScreen.kt b/app/src/main/java/com/kuit/afternote/feature/home/presentation/screen/HomeScreen.kt index e20b35d3..7c58c2d6 100644 --- a/app/src/main/java/com/kuit/afternote/feature/home/presentation/screen/HomeScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/home/presentation/screen/HomeScreen.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.kuit.afternote.R import com.kuit.afternote.core.ui.component.navigation.BottomNavItem import com.kuit.afternote.core.ui.component.navigation.BottomNavigationBar +import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.MindRecordHomeContract import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.MindRecordViewModel import com.kuit.afternote.feature.home.presentation.component.CalendarDay import com.kuit.afternote.feature.home.presentation.component.CalendarDayStyle @@ -36,12 +37,16 @@ import com.kuit.afternote.feature.home.presentation.component.HomeHeader import com.kuit.afternote.feature.home.presentation.component.HomeInfoCard import com.kuit.afternote.feature.home.presentation.component.RecipientBadge import com.kuit.afternote.feature.home.presentation.component.WeeklyCalendarStrip +import com.kuit.afternote.feature.user.presentation.uimodel.ProfileUiState +import com.kuit.afternote.feature.user.presentation.viewmodel.ProfileEditViewModelContract import com.kuit.afternote.feature.user.presentation.viewmodel.ProfileViewModel import com.kuit.afternote.ui.theme.AfternoteTheme import com.kuit.afternote.ui.theme.Gray1 import com.kuit.afternote.ui.theme.Gray5 import com.kuit.afternote.ui.theme.Gray9 import com.kuit.afternote.ui.theme.Sansneo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.Locale @@ -69,8 +74,8 @@ interface HomeScreenEvent { fun HomeScreen( modifier: Modifier = Modifier, event: HomeScreenEvent = EmptyHomeScreenEvent, - profileViewModel: ProfileViewModel = hiltViewModel(), - recordViewModel: MindRecordViewModel = hiltViewModel() + profileViewModel: ProfileEditViewModelContract = hiltViewModel(), + recordViewModel: MindRecordHomeContract = hiltViewModel() ) { var content by remember { mutableStateOf(HomeScreenContent()) } val uiState = profileViewModel.uiState.collectAsStateWithLifecycle() @@ -91,6 +96,11 @@ fun HomeScreen( Scaffold( modifier = modifier.fillMaxSize(), containerColor = Gray1, + topBar = { + HomeHeader( + onSettingsClick = event::onSettingsClick + ) + }, bottomBar = { BottomNavigationBar( selectedItem = BottomNavItem.HOME, @@ -104,12 +114,6 @@ fun HomeScreen( .padding(paddingValues) .verticalScroll(rememberScrollState()) ) { - // Header - HomeHeader( - onProfileClick = event::onProfileClick, - onSettingsClick = event::onSettingsClick - ) - // Greeting section GreetingSection(userName = uiState.value.name) @@ -192,6 +196,7 @@ private fun GreetingSection( modifier = Modifier.padding(horizontal = 20.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { + Spacer(Modifier.height(24.dp)) Text( text = stringResource(R.string.home_greeting, userName), fontFamily = Sansneo, @@ -229,12 +234,65 @@ private fun SectionTitle( } private object EmptyHomeScreenEvent : HomeScreenEvent { - override fun onBottomNavTabSelected(item: BottomNavItem) = Unit - override fun onProfileClick() = Unit - override fun onSettingsClick() = Unit - override fun onDailyQuestionCtaClick() = Unit - override fun onTimeLetterClick() = Unit - override fun onAfterNoteClick() = Unit + override fun onBottomNavTabSelected(item: BottomNavItem) { + // No-op: default event when no callback is provided. + } + + override fun onProfileClick() { + // No-op: default event when no callback is provided. + } + + override fun onSettingsClick() { + // No-op: default event when no callback is provided. + } + + override fun onDailyQuestionCtaClick() { + // No-op: default event when no callback is provided. + } + + override fun onTimeLetterClick() { + // No-op: default event when no callback is provided. + } + + override fun onAfterNoteClick() { + // No-op: default event when no callback is provided. + } +} + +/** Fake ProfileEditViewModelContract for Preview (no Hilt). */ +private class FakeProfileEditViewModel : ProfileEditViewModelContract { + private val _uiState = MutableStateFlow(ProfileUiState(name = "박서연")) + override val uiState: StateFlow = _uiState + override fun loadProfile() { + // No-op: Fake for Preview only; no API call. + } + + override fun updateProfile( + name: String?, + phone: String?, + profileImageUrl: String?, + pickedProfileImageUri: String? + ) { + // No-op: Fake for Preview only; no state update. + } + + override fun setSelectedProfileImageUri(uri: android.net.Uri?) { + // No-op: Fake for Preview only. + } + + override fun clearUpdateSuccess() { + // No-op: Fake for Preview only. + } +} + +/** Fake MindRecordHomeContract for Preview (no Hilt). */ +private class FakeMindRecordHomeViewModel : MindRecordHomeContract { + override val calendarDays: StateFlow> = + MutableStateFlow(defaultCalendarDays()) + + override fun loadRecordsForDiaryList() { + // No-op: Fake for Preview only; no API call. + } } internal fun defaultCalendarDays(): List = listOf( @@ -251,6 +309,9 @@ internal fun defaultCalendarDays(): List = listOf( @Composable private fun HomeScreenPreview() { AfternoteTheme { - HomeScreen() + HomeScreen( + profileViewModel = remember { FakeProfileEditViewModel() }, + recordViewModel = remember { FakeMindRecordHomeViewModel() } + ) } } diff --git a/app/src/main/res/drawable/ic_home_topbar_logo.xml b/app/src/main/res/drawable/ic_home_topbar_logo.xml new file mode 100644 index 00000000..4db54688 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_topbar_logo.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/setting.png b/app/src/main/res/drawable/setting.png deleted file mode 100644 index 6ab1715b..00000000 Binary files a/app/src/main/res/drawable/setting.png and /dev/null differ diff --git a/app/src/main/res/drawable/setting.xml b/app/src/main/res/drawable/setting.xml new file mode 100644 index 00000000..9dc353f8 --- /dev/null +++ b/app/src/main/res/drawable/setting.xml @@ -0,0 +1,24 @@ + + + + + + +