diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 92c69340..de974a34 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,38 +9,18 @@ val keystoreProperties = Properties() keystoreProperties.load(FileInputStream(rootProject.file("app/keystore.properties"))) plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.musicroad.android.application) alias(libs.plugins.ksp) alias(libs.plugins.hilt) alias(libs.plugins.google.services) - alias(libs.plugins.kotlin.serialization) alias(libs.plugins.firebase.crashlytics) } android { namespace = "com.squirtles.musicroad" - compileSdk = 34 defaultConfig { - applicationId = "com.squirtles.musicroad" - minSdk = 26 - targetSdk = 34 - versionCode = 10100 - versionName = "1.1.0" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary = true - } - addManifestPlaceholders(mapOf("NAVERMAP_CLIENT_ID" to properties.getProperty("NAVERMAP_CLIENT_ID"))) - - buildConfigField( - "String", - "GOOGLE_CLIENT_ID", - "\"${properties.getProperty("GOOGLE_CLIENT_ID")}\"" - ) } signingConfigs { @@ -75,20 +55,9 @@ android { signingConfig = signingConfigs.getByName("signedRelease") } } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" - } buildFeatures { viewBinding = true buildConfig = true - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.1" } packaging { resources { @@ -98,73 +67,38 @@ android { } dependencies { - implementation(projects.domain) - implementation(projects.data) - implementation(projects.mediaservice) + implementation(projects.core.navigation) + implementation(projects.data.applemusic) + implementation(projects.data.firebase) + implementation(projects.data.user) + implementation(projects.data.pick) + implementation(projects.data.favorite) + implementation(projects.data.location) + implementation(projects.data.order) + implementation(projects.feature.main) + implementation(projects.feature.userinfo) + implementation(projects.feature.search) + implementation(projects.feature.create) + implementation(projects.feature.favorite) + implementation(projects.feature.mypick) + implementation(projects.feature.detail) + implementation(projects.feature.map) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.ui) - implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) - implementation(libs.androidx.material.icons.extended) - implementation(libs.androidx.constraintlayout) - implementation(libs.androidx.ui.viewbinding) - implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.core.splashscreen) - implementation(libs.androidx.compose.material) - implementation(libs.androidx.animation) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.ui.test.junit4) - debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.androidx.ui.test.manifest) - implementation(libs.kotlinx.immutable) // Hilt implementation(libs.hilt.android) - ksp(libs.hilt.android.compiler) androidTestImplementation(libs.hilt.android.testing) + ksp(libs.hilt.android.compiler) kspAndroidTest(libs.hilt.android.compiler) - implementation(libs.androidx.hilt.navigation.compose) - implementation(libs.androidx.hilt.navigation.fragment) // Firebase implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) - implementation(libs.firebase.auth.ktx) implementation(libs.google.firebase.dynamic.module.support) implementation(libs.firebase.crashlytics) - - // Map - implementation(libs.map.sdk) - implementation(libs.play.services.location) - - // Coil - implementation(libs.coil) - implementation(libs.coil.compose) - implementation(libs.coil.network.okhttp) - - // ExoPlayer - implementation(libs.androidx.media3.exoplayer) - implementation(libs.androidx.media3.exoplayer.dash) - implementation(libs.androidx.media3.ui) - implementation(libs.androidx.media3.session) - - // Paging - implementation(libs.androidx.paging.runtime) - implementation(libs.androidx.paging.compose.android) - - // Serialization - implementation(libs.kotlinx.serialization.json) - - // Credentials - implementation(libs.androidx.credentials) - implementation(libs.androidx.credentials.play.services.auth) - implementation(libs.googleid) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c314c2a..89aba6a1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ tools:targetApi="31"> @@ -35,16 +35,16 @@ android:name="com.naver.maps.map.CLIENT_ID" android:value="${NAVERMAP_CLIENT_ID}" /> - - - + + + + + + - - - + + + diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/music/visualizer/BaseVisualizer.kt b/app/src/main/java/com/squirtles/musicroad/detail/components/music/visualizer/BaseVisualizer.kt deleted file mode 100644 index 341d7e77..00000000 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/music/visualizer/BaseVisualizer.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.squirtles.musicroad.detail.components.music.visualizer - -import android.media.audiofx.Visualizer -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlin.math.sqrt - -class BaseVisualizer { - private var visualizer: Visualizer? = null - - private val _fftFlow = MutableSharedFlow>(replay = 1) - val fftFlow: SharedFlow> = _fftFlow.asSharedFlow() - - private val validRange = getSignificantFftIndexRange() - - fun setVisualizer(audioSessionId: Int) { - val visualizer = Visualizer(audioSessionId) - this.visualizer = visualizer - } - - fun setVisualizerListener() { - visualizer?.run { - enabled = false - captureSize = CAPTURE_SIZE - setDataCaptureListener(object : Visualizer.OnDataCaptureListener { - override fun onWaveFormDataCapture(visualizer: Visualizer, bytes: ByteArray, samplingRate: Int) { - // NOT USED - } - - override fun onFftDataCapture(visualizer: Visualizer, bytes: ByteArray, samplingRate: Int) { - // bytes = [실수부,허수부,실수부,허수부 ....] - val size = bytes.size / 4 // 각 복소수당 2바이트 - val magnitudes = FloatArray(size) - - for (i in 0 until size) { - val real = bytes[2 * i].toInt() - val imaginary = bytes[2 * i + 1].toInt() - magnitudes[i] = sqrt((real * real + imaginary * imaginary).toDouble()).toFloat() - } - - val filteredMagnitudes = magnitudes.copyOfRange(validRange.first, validRange.last + 1) - _fftFlow.tryEmit(filteredMagnitudes.toList()) - } - }, Visualizer.getMaxCaptureRate() / 2, false, true) - enabled = true - } - } - - fun release() { - visualizer?.release() - visualizer = null - } - - // 20 ~ 4000 Hz 사이만 필터링 - private fun getSignificantFftIndexRange( - samplingRate: Int = SAMPLING_RATE, - captureSize: Int = CAPTURE_SIZE, - minFreq: Int = MIN_FREQ, - maxFreq: Int = MAX_FREQ - ): IntRange { - val resolution = samplingRate.toDouble() / captureSize - val startIndex = (minFreq / resolution).toInt() - val endIndex = (maxFreq / resolution).toInt() - return startIndex..endIndex - } - - companion object { - const val CAPTURE_SIZE = 512 - const val SAMPLING_RATE = 22000 - const val MIN_FREQ = 20 - const val MAX_FREQ = 4500 - } -} diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/music/visualizer/CanvasCircleVisualizer.kt b/app/src/main/java/com/squirtles/musicroad/detail/components/music/visualizer/CanvasCircleVisualizer.kt deleted file mode 100644 index 12d9b634..00000000 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/music/visualizer/CanvasCircleVisualizer.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.squirtles.musicroad.detail.components.music.visualizer - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.PointMode -import androidx.compose.ui.graphics.drawscope.Stroke -import kotlin.math.cos -import kotlin.math.max -import kotlin.math.sin - -@Composable -internal fun CanvasCircle( - color: Color, - modifier: Modifier = Modifier -) { - Canvas(modifier = modifier.fillMaxSize()) { - val width = size.width - val height = size.height - val radius = max(width, height) * 0.4f - - drawCircle( - color = color, - radius = radius, - style = Stroke(width = 4f) - ) - } -} - -/* Bar 형태 원형 시각화 */ -@Composable -internal fun CanvasSoundEffectBar( - audioData: List, - color: Color, - sizeRatio: Float, - modifier: Modifier = Modifier -) { - - val offsetAngle = Math.toRadians(-90.0) - val angleStep = 360f / audioData.size - val points = mutableListOf() - - Canvas(modifier = modifier.fillMaxSize()) { - val width = size.width - val height = size.height - val radius = max(width, height) * sizeRatio - - audioData.forEachIndexed { i, magnitude -> - // 현재 오디오데이터가 그려질 각도 - val angle = Math.toRadians((i * angleStep).toDouble()) + offsetAngle - val cosValue = cos(angle) - val sinValue = sin(angle) - val barHeight = magnitude * height / 5 - - // 현재 오디오데이터가 그려질 xy 좌표 - val startX = (width / 2 + radius * cosValue).toFloat() - val startY = (height / 2 + radius * sinValue).toFloat() - val endX = (width / 2 + (radius + barHeight) * cosValue).toFloat() - val endY = (height / 2 + (radius + barHeight) * sinValue).toFloat() - - points.add(Offset(startX, startY)) - points.add(Offset(endX, endY)) - } - - drawPoints( - points = points, - pointMode = PointMode.Lines, - color = color, - strokeWidth = 24f - ) - } -} - -/* Wave 형태 원형 시각화 */ -@Composable -internal fun CanvasSoundEffectWave( - audioData: List, - color: Color, - modifier: Modifier = Modifier -) { - - val offsetAngle = Math.toRadians(-90.0) - val angleStep = 360f / audioData.size - val path = Path() - - Canvas(modifier = modifier.fillMaxSize()) { - val width = size.width - val height = size.height - val radius = max(width, height) * 0.45f - - audioData.forEachIndexed { i, magnitude -> - val angle = Math.toRadians((i * angleStep).toDouble()) + offsetAngle - val cosValue = cos(angle) - val sinValue = sin(angle) - val barHeight = magnitude * height / 5 - - val startX = (width / 2 + radius * cosValue).toFloat() - val startY = (height / 2 + radius * sinValue).toFloat() - val endX = (width / 2 + (radius + barHeight) * cosValue).toFloat() - val endY = (height / 2 + (radius + barHeight) * sinValue).toFloat() - - if (i == 0) { - path.moveTo(startX, startY) - } else { - - path.lineTo(endX, endY) - } - } - - drawPath( - path = path, - color = color, - style = Stroke(width = 4f) - ) - } -} - -private const val STROKE_WIDTH = 18f diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/music/visualizer/CircleVisualizer.kt b/app/src/main/java/com/squirtles/musicroad/detail/components/music/visualizer/CircleVisualizer.kt deleted file mode 100644 index 39dd6a9b..00000000 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/music/visualizer/CircleVisualizer.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.squirtles.musicroad.detail.components.music.visualizer - -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector1D -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.tween -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import com.squirtles.musicroad.ui.theme.White -import kotlinx.coroutines.launch - -@Composable -fun CircleVisualizer( - baseVisualizer: () -> BaseVisualizer, - audioSessionId: Int, - color: Color = White, - sizeRatio: Float, - modifier: Modifier = Modifier -) { - val magnitudes = remember { mutableStateOf>>(emptyList()) } - val visualizer = baseVisualizer() - - LaunchedEffect(Unit) { - visualizer.setVisualizer(audioSessionId) - visualizer.setVisualizerListener() - visualizer.fftFlow.collect { fftArray -> - val normalizedData = processAudioData(fftArray) - - if (magnitudes.value.isEmpty()) { - magnitudes.value = normalizedData.map { Animatable(it) } - } else { - normalizedData.forEachIndexed { i, magnitude -> - launch { - magnitudes.value[i].animateTo( - targetValue = magnitude, - animationSpec = tween( - durationMillis = 120, - easing = FastOutSlowInEasing - ) - ) - } - } - } - } - } - - DisposableEffect(Unit) { - onDispose { - visualizer.release() - } - } - - CanvasSoundEffectBar( - audioData = magnitudes.value.map { it.value }, - color = color, - sizeRatio = sizeRatio, - modifier = modifier - ) -} - -private fun processAudioData(audioData: List): List { - val scaledData = scaleAudioData(audioData) - return normalizeAudioData(scaledData) -} - -/* 주파수 대역별 가중치 */ -private fun scaleAudioData(audioData: List): List { - val size = audioData.size - return audioData.mapIndexed { index, value -> - val scaleFactor = when { - index < size / 8 -> 0.4f - index < size / 4 -> 1.0f // 저주파 대역 - index < size / 2 -> 2.0f // 중간 대역 - index < size / 1.33 -> 3.0f // 고주파 대역 - else -> 5.0f // 고주파 대역 - } - value * scaleFactor - } -} - -private fun applyLogScale(audioData: List): List { - val epsilon = 1e-6f // 0 방지용 작은 값 - val minValue = audioData.minOrNull() ?: 0f - val offset = if (minValue < 1f) 1f - minValue else 0f // 최소값을 1로 이동 - - return audioData.map { value -> - val shiftedValue = value + offset + epsilon // 데이터를 양수 범위로 이동 - 20 * kotlin.math.log10(shiftedValue) // 로그 변환 - } -} - -/* 다이나믹 레인지 압축 */ -private fun compressDynamicRangeRoot(audioData: List): List { - return audioData.map { kotlin.math.sqrt(it) } -} - -/* 0.0 ~ 1.0 사이 정규화 */ -private fun normalizeAudioData(audioData: List): List { - val max = audioData.maxOrNull() ?: 1f // 데이터 최대값 - val min = audioData.minOrNull() ?: 0f // 데이터 최소값 - return if (max - min <= 1f) List(audioData.size) { 0f } else audioData.map { (it - min) / (max - min) } -} - diff --git a/app/src/main/java/com/squirtles/musicroad/map/navigation/MapNavigation.kt b/app/src/main/java/com/squirtles/musicroad/map/navigation/MapNavigation.kt deleted file mode 100644 index 507103d6..00000000 --- a/app/src/main/java/com/squirtles/musicroad/map/navigation/MapNavigation.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.squirtles.musicroad.map.navigation - -import android.content.Context -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import androidx.navigation.toRoute -import com.squirtles.musicroad.map.MapScreen -import com.squirtles.musicroad.map.MapViewModel -import com.squirtles.musicroad.media.PlayerServiceViewModel -import com.squirtles.musicroad.navigation.MapRoute -import com.squirtles.musicroad.navigation.Route -import com.squirtles.musicroad.detail.PickDetailScreen - -fun NavController.navigateMap(navOptions: NavOptions? = null) { - navigate(Route.Map, navOptions) -} - -fun NavController.navigatePickDetail(pickId: String, navOptions: NavOptions? = null) { - navigate(MapRoute.PickDetail(pickId), navOptions) -} - -fun NavGraphBuilder.mapNavGraph( - mapViewModel: MapViewModel, - playerServiceViewModel: PlayerServiceViewModel, - onFavoriteClick: (String) -> Unit, - onCenterClick: () -> Unit, - onUserInfoClick: (String) -> Unit, - onPickSummaryClick: (String) -> Unit, - onBackClick: () -> Unit, - onDeleted: (Context) -> Unit, -) { - composable { - MapScreen( - mapViewModel = mapViewModel, - playerServiceViewModel = playerServiceViewModel, - onFavoriteClick = onFavoriteClick, - onCenterClick = onCenterClick, - onUserInfoClick = onUserInfoClick, - onPickSummaryClick = onPickSummaryClick, - ) - } - - composable { backStackEntry -> - val pickId = backStackEntry.toRoute().pickId - - PickDetailScreen( - pickId = pickId, - playerServiceViewModel = playerServiceViewModel, - onUserInfoClick = onUserInfoClick, - onBackClick = onBackClick, - onDeleted = onDeleted, - ) - } -} diff --git a/app/src/main/java/com/squirtles/musicroad/navigation/SearchRoute.kt b/app/src/main/java/com/squirtles/musicroad/navigation/SearchRoute.kt deleted file mode 100644 index 1765ecea..00000000 --- a/app/src/main/java/com/squirtles/musicroad/navigation/SearchRoute.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.squirtles.musicroad.navigation - -import androidx.lifecycle.SavedStateHandle -import androidx.navigation.toRoute -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.utils.serializableType -import kotlinx.serialization.Serializable -import kotlin.reflect.typeOf - -@Serializable -sealed interface SearchRoute : Route { - @Serializable - data class Create(val song: Song) : SearchRoute { - companion object { - val typeMap = mapOf(typeOf() to serializableType()) - - fun from(savedStateHandle: SavedStateHandle) = - savedStateHandle.toRoute(typeMap) - } - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index c201b1df..00000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,158 +0,0 @@ - - MusicRoad - - - 위치와 마이크 권한이 필요합니다. - 설정에서 권한을 허용해 주세요. - 네트워크 연결이 원활하지 않습니다. - 유저 정보가 존재하지 않습니다. 앱을 재설치 해주세요. - 유저 등록 실패. 문의해주세요 - 권한 요청 - 앱 실행을 위해\n위치와 마이크 권한이 필요합니다. - 확인 - 설정(앱 정보)에서 권한을 허용해주세요. - 설정으로 이동 - - - 픽 보관함 이동 버튼 아이콘 - 설정 이동 버튼 아이콘 - 내비게이션 중앙 버튼 아이콘 - 🎧 주변에 %d개의 픽이 있습니다! - 🔇 주변에 %d개의 픽이 있습니다! - 앨범 이미지 - 님의 픽 - 픽을 담은 개수 - 내가 - 등록한 픽 - 데이터를 불러오는 데 일시적인 오류가 발생했습니다. - - - 픽 등록 - 님의 픽 - 등록하기 - 앨범 이미지 - 뒤로 가기 - 거리에 남길 한마디를 입력하세요. - 등록된 한마디가 없습니다. - 삭제하기 - 담은 픽 - 픽 담기 - 픽을 담은 개수 - 밀어서 뮤직비디오 보기 - 5초 앞으로 - 5초 뒤로 - 재생/일시정지 - Apple Music에서 제공하는 30초 미리보기 영상입니다. - 480p - 삭제되었습니다 - 픽 보관함에 추가되었습니다. - 픽 보관함에서 제거되었습니다. - 담은 픽이 없습니다 - 등록한 픽이 없습니다 - 일시적인 오류가 발생했습니다. - - - 취소 - 선택 삭제 - 원 윤곽선이 있는 체크 아이콘 - 전체 - 선택 - - - 최근 등록순 - 최근 담은순 - 과거 담은순 - 과거 등록순 - 담기 많은순 - 체크 아이콘 - 닫기 - - - 검색 - 검색 결과 - 노래 앨범 이미지 - 노래 검색 버튼 - 검색 결과가 없습니다. - 검색 결과를 불러올 수 없습니다. - - - 현재 위치 로딩 중... - 종료 - - - 삭제하시겠습니까? - 등록하신 픽이 삭제됩니다. - 취소 - 삭제하기 - - 픽 보관함에서 %d개의 픽이 삭제됩니다. - 선택하신 %d개의 픽이 삭제됩니다. - - - 상단 바 뒤로 가기 버튼 - 픽 목록 편집 모드 활성화 버튼 - 전체 선택 - 선택 해제 - 픽 보관함 - 등록한 픽 - - - 메뉴로 이동하기 아이콘 - 프로필 이미지 - Pick - 픽 보관함 - 등록한 픽 - 픽 보관함 메뉴 아이콘 - 등록한 픽 메뉴 아이콘 - Settings - 프로필 - 알림 - 로그아웃 - 회원 탈퇴 - 프로필 설정 메뉴 아이콘 - 알림 메뉴 아이콘 - 로그아웃 메뉴 아이콘 - 지도 아이콘 - 지도로 돌아가기 - - - 알림 설정 - 준비 중인 기능입니다! - 프로필 설정 - - 닉네임 - 닉네임에는 한글, 영문, 숫자만 사용할 수 있습니다. - 닉네임을 2글자 이상 입력해주세요. - 닉네임은 10글자 이하로 가능합니다. - 변경 사항 적용 - 변경 사항이 적용되었습니다. - 일시적인 오류가 발생했습니다. - - - 로그인 - Sign in with Google - Google Logo - - - 더 많은 기능을 이용하기 위해\n로그인이 필요합니다 - 담은 픽을 확인하기 위해\n로그인이 필요합니다 - 픽을 등록하기 위해\n로그인이 필요합니다 - 픽을 담기 위해\n로그인이 필요합니다 - 픽을 담기 위해\n로그인이 필요합니다 - 로그인이 필요합니다 - 취소 - 로그인에 실패했습니다 - 기기에 로그인된 구글 계정이 없습니다 - Google 로그인을 사용할 수 없는 기기입니다 - - - 로그아웃 하시겠습니까? - 취소 - 로그아웃 - - - 정말 탈퇴하시겠습니까? - 탈퇴 시 회원 정보와 픽 데이터가 삭제되며, 복구할 수 없습니다. - 취소 - 탈퇴 - diff --git a/data/.gitignore b/audio_visualizer/.gitignore similarity index 100% rename from data/.gitignore rename to audio_visualizer/.gitignore diff --git a/mediaservice/build.gradle.kts b/audio_visualizer/build.gradle.kts similarity index 59% rename from mediaservice/build.gradle.kts rename to audio_visualizer/build.gradle.kts index 5907a17d..ef6a7a98 100644 --- a/mediaservice/build.gradle.kts +++ b/audio_visualizer/build.gradle.kts @@ -1,12 +1,10 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.jetbrains.kotlin.android) - alias(libs.plugins.ksp) - alias(libs.plugins.hilt) } android { - namespace = "com.squirtles.mediaservice" + namespace = "com.miller198.audiovisualizer" compileSdk = 34 defaultConfig { @@ -15,7 +13,9 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } - + buildFeatures { + compose = true + } buildTypes { release { isMinifyEnabled = false @@ -23,11 +23,14 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.14" } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } } @@ -36,18 +39,13 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) + implementation(libs.androidx.foundation.android) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) - //hilt - implementation(libs.hilt.android) - ksp(libs.hilt.android.compiler) - androidTestImplementation(libs.hilt.android.testing) - implementation(libs.inject) - - //media3 - implementation(libs.androidx.media3.exoplayer) - implementation(libs.androidx.media3.exoplayer.dash) - implementation(libs.androidx.media3.session) + implementation(platform(libs.compose.bom)) + implementation(libs.bundles.compose) + implementation(libs.androidx.foundation.layout.android) + implementation(libs.androidx.animation.core.android) } diff --git a/data/consumer-rules.pro b/audio_visualizer/consumer-rules.pro similarity index 100% rename from data/consumer-rules.pro rename to audio_visualizer/consumer-rules.pro diff --git a/data/proguard-rules.pro b/audio_visualizer/proguard-rules.pro similarity index 100% rename from data/proguard-rules.pro rename to audio_visualizer/proguard-rules.pro diff --git a/audio_visualizer/src/androidTest/java/com/miller198/audiovisualizer/ExampleInstrumentedTest.kt b/audio_visualizer/src/androidTest/java/com/miller198/audiovisualizer/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..bb044e31 --- /dev/null +++ b/audio_visualizer/src/androidTest/java/com/miller198/audiovisualizer/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.miller198.audiovisualizer + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.miller198.audiovisualizer.test", appContext.packageName) + } +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/BaseVisualizer.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/BaseVisualizer.kt new file mode 100644 index 00000000..c7b6db50 --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/BaseVisualizer.kt @@ -0,0 +1,113 @@ +package com.miller198.audiovisualizer + +import android.media.audiofx.Visualizer +import android.util.Log +import com.miller198.audiovisualizer.configs.VisualizerCallbacks + +/** + * Core class for capturing audio data using Android's [Visualizer] API. + * + * This class handles setup, configuration, and lifecycle management of a [Visualizer] instance. + */ +class BaseVisualizer { + private var visualizer: Visualizer? = null + + /** + * Initializes and starts the [Visualizer] with the given configuration. + * + * @param audioSessionId The audio session ID to capture audio from. + * @param captureSize The number of bytes to capture. Must be a power of two and within supported range. + * @param useWaveCapture Whether to enable waveform data capture. + * @param useFftCapture Whether to enable FFT (frequency) data capture. + * @param visualizerCallbacks Callbacks to receive waveform and/or FFT data. + * @param captureRate Capture rate in milliseconds. Defaults to [Visualizer.getMaxCaptureRate]. + */ + fun start( + audioSessionId: Int, + captureSize: Int, + useWaveCapture: Boolean, + useFftCapture: Boolean, + visualizerCallbacks: VisualizerCallbacks, + captureRate: Int = Visualizer.getMaxCaptureRate(), + ) { + stop() + setVisualizer(audioSessionId) + + // Validate capture size and fallback to max size if needed + visualizer?.captureSize = + if (!isPowerOfTwo(captureSize) || !isValidCaptureSize(captureSize)) { + Visualizer.getCaptureSizeRange()[1].also { + Log.w("BaseVisualizer", "Invalid capture size, fallback to max: $it") + } + } else { + captureSize + } + + setVisualizerListener( + useWaveCapture, + useFftCapture, + captureRate, + visualizerCallbacks.provideVisualizerCallbacks() + ) + } + + /** + * Returns whether the visualizer is currently running. + */ + fun isRunning(): Boolean = visualizer != null + + /** + * Stops and releases the [Visualizer] + */ + fun stop() { + visualizer?.release() + visualizer = null + } + + /** + * Creates and assigns a [Visualizer] instance for the given audio session. + */ + private fun setVisualizer(audioSessionId: Int) { + try { + visualizer = Visualizer(audioSessionId) + } catch (e: RuntimeException) { + Log.e("BaseVisualizer", "Failed to create Visualizer", e) + } + } + + /** + * Registers a [Visualizer.OnDataCaptureListener] to receive waveform and/or FFT data. + */ + private fun setVisualizerListener( + isWaveCapture: Boolean, + isFftCapture: Boolean, + captureRate: Int, + dataCaptureListener: Visualizer.OnDataCaptureListener, + ) { + visualizer?.run { + enabled = false + setDataCaptureListener( + dataCaptureListener, + captureRate, + isWaveCapture, + isFftCapture + ) + enabled = true + } + } + + /** + * Checks whether the provided [captureSize] is a power of two. + */ + private fun isPowerOfTwo(captureSize: Int): Boolean { + return captureSize > 0 && (captureSize and (captureSize - 1)) == 0 + } + + /** + * Checks whether the provided [captureSize] is within the valid range supported by [Visualizer]. + */ + private fun isValidCaptureSize(captureSize: Int): Boolean { + val range = Visualizer.getCaptureSizeRange() + return captureSize in range[0]..range[1] + } +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/FftDataProcessor.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/FftDataProcessor.kt new file mode 100644 index 00000000..50f202be --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/FftDataProcessor.kt @@ -0,0 +1,217 @@ +package com.miller198.audiovisualizer + +import android.media.audiofx.Visualizer +import kotlin.math.hypot +import kotlin.math.log +import kotlin.math.pow +import kotlin.math.sqrt + +/** + * Class for processing FFT (Fast Fourier Transform) data of the result of Visualizer capture. + * + * @param rawFftBytes The raw FFT data as a byte array captured by [Visualizer.OnDataCaptureListener.onFftDataCapture]. + */ +class FftDataProcessor(rawFftBytes: ByteArray) { + // The Fft data applied pre-processing function + private var processedData: List = emptyList() + + init { + processedData = calculateFftMagnitude(rawFftBytes) + } + + /** + * **This function must be used before applying any other preprocessing functions.** + * + * Calculates the FFT (Fast Fourier Transform) magnitude spectrum from raw FFT byte data. + * + * @param bytes The audio data as a byte array captured by [Visualizer.OnDataCaptureListener.onFftDataCapture]. + * The array alternates real and imaginary parts: [real0, imag0, real1, imag1, ...]. + * + * @return A list of FFT magnitudes (in linear scale), excluding the first two values (DC and Nyquist). + * + * Note: The first two bytes represent the DC component and the Nyquist frequency and are excluded. + * If you need more information about the result of FFT capture, see [Visualizer.getFft]. + */ + fun calculateFftMagnitude(bytes: ByteArray): List { + val audioData = bytes.drop(2).map { it.toDouble() } + + val size = audioData.size / 2 + val magnitudes = FloatArray(size) + + for (i in 0 until size) { + val real = audioData.getOrNull(2 * i) ?: 0.0 + val imaginary = audioData.getOrNull(2 * i + 1) ?: 0.0 + magnitudes[i] = hypot(real, imaginary).toFloat() + } + + return magnitudes.toList() + } + + /** + * Filters the given frequency spectrum data to include only the components within a specified frequency range. + * + * @param samplingRate The sampling rate of the original audio signal (in Hz). + * @param captureSize The size of the FFT window used when capturing the audio data. + * @param minFreq The minimum frequency (in Hz) to include in the result. + * @param maxFreq The maximum frequency (in Hz) to include in the result. + * + * @return A list of magnitudes corresponding to the frequency components within [minFreq, maxFreq]. + * + * The frequency resolution is calculated as `samplingRate / captureSize`, and the corresponding index + * range is computed to extract the subset of the frequency data. + */ + fun filterFrequency( + samplingRate: Int, + captureSize: Int, + minFreq: Int, + maxFreq: Int + ): FftDataProcessor { + val resolution = ((samplingRate / 2.0) / (captureSize / 2 - 1)) + val startIndex = (minFreq / resolution).toInt().coerceIn(0, processedData.lastIndex) + val endIndex = (maxFreq / resolution).toInt().coerceIn(startIndex, processedData.lastIndex) + + processedData = processedData.slice(startIndex..endIndex) + + return this + } + + /** + * Applies scaling weights to audio frequency data based on predefined frequency range ratios. + * + * Each element in the audio data is scaled according to which ratio range it falls into, + * as defined by the provided list of [FrequencyScale]s. + * + * @param frequencyScales A list of [FrequencyScale] objects defining the ratio ranges and their associated weights. + * + * @return A new list of Float values with each element scaled according to its frequency range. + */ + fun scaleFrequencies( + frequencyScales: List + ): FftDataProcessor { + val size = processedData.size + + processedData = processedData.mapIndexed { index, value -> + val ratio = index.toFloat() / size + val scale = frequencyScales.find { ratio in it.rangeRatio }?.weight ?: 1.0f + value * scale + } + + return this + } + + /** + * Applies a logarithmic scale transformation to the given audio magnitude data. + * for compressing large dynamic ranges in audio signals, + * + * A small epsilon is added to avoid log(0), and an offset is used to ensure + * that all input values are positive before applying the logarithm. + * + * @param base The base of the logarithm to apply (must be > 0 and ≠ 1; default is 10). + * @param scaleFactor A multiplier applied after the logarithm for additional scaling (default is 1). + * @return A list of audio magnitudes scaled logarithmically. + */ + fun applyLogScale( + base: Float = 10f, + scaleFactor: Float = 1f + ): FftDataProcessor { + require(base > 0f && base != 1f) { "Logarithm base must be greater than 0 and not equal to 1." } + + val epsilon = 1e-6f // to avoid log(0) + val minValue = processedData.minOrNull() ?: 0f + val offset = if (minValue < 1f) 1f - minValue else 0f // move the minimum value to 1 + + processedData = processedData.map { value -> + val shifted = value + offset + epsilon + val safeValue = if (shifted <= 0f || shifted.isNaN()) epsilon else shifted + scaleFactor * log(safeValue, base) + } + + return this + } + + /** + * Applies dynamic range compression using square root scaling. + * + * This method is useful for reducing the impact of high-magnitude peaks + * and enhancing lower-magnitude values, making the overall data more perceptually uniform. + * + * @return A list of compressed audio data using sqrt scaling. + * + * Note: Negative input values are clamped to 0 to avoid sqrt domain errors. + */ + fun compressDynamicRangeRoot(): FftDataProcessor { + processedData = processedData.map { value -> + sqrt(value.coerceAtLeast(0f)) + } + return this + } + + /** + * Applies Z-Score normalization to the given audio data. + * + * This method standardizes the data to have zero mean and unit variance, + * making it easier to compare values across different datasets or features. + * + * @return A list of normalized values centered around 0 with unit variance. + * If input is empty or standard deviation is zero, returns a list of zeros. + */ + fun normalizeByZScore(): FftDataProcessor { + if (processedData.isEmpty()) return this + + val mean = processedData.average().toFloat() + val stdDev = sqrt(processedData.map { (it - mean).pow(2) }.average()).toFloat() + + processedData = if (stdDev == 0f || stdDev.isNaN()) { + List(processedData.size) { 0f } + } else { + processedData.map { (it - mean) / stdDev } + } + + return this + } + + /** + * Applies Max-Min Normalization to the given audio data. + * + * @return A list of normalized values between 0 and 1. + */ + fun normalize(): FftDataProcessor { + val max = processedData.maxOrNull() ?: 1f + val min = processedData.minOrNull() ?: 0f + processedData = if (max - min <= 1f) List(processedData.size) { 0f } else processedData.map { (it - min) / (max - min) } + + return this + } + + fun result(): List = processedData +} + +/** + * Default pre-processing for FFT data. + * + * @param bytes The input byte array containing FFT data. + * @param captureSize The size of the FFT capture. + * @param minFreq The minimum frequency to include in the FFT data. + * @param maxFreq The maximum frequency to include in the FFT data. + * @param samplingRate The sampling rate of the audio signal. + * @return A list of pre-processed FFT data. + */ +fun defaultPreProcessFftData( + bytes: ByteArray, + captureSize: Int, + minFreq: Int, + maxFreq: Int, + samplingRate: Int +): List { + return FftDataProcessor(bytes) + .filterFrequency( + samplingRate / 1000, + captureSize, + minFreq, + maxFreq + ) + .applyLogScale() + .normalizeByZScore() + .normalize() + .result() +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/FrequencyScale.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/FrequencyScale.kt new file mode 100644 index 00000000..ee703742 --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/FrequencyScale.kt @@ -0,0 +1,12 @@ +package com.miller198.audiovisualizer + +/** + * Represents a frequency range (as a ratio of the full signal) and its corresponding scaling weight. + * + * @property rangeRatio A ratio-based range from 0.0 to 1.0 indicating the portion of the frequency spectrum. + * @property weight The scaling factor to apply to values within this range. + */ +data class FrequencyScale( + val rangeRatio: ClosedFloatingPointRange, + val weight: Float +) diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/configs/Configs.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/configs/Configs.kt new file mode 100644 index 00000000..a0328598 --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/configs/Configs.kt @@ -0,0 +1,212 @@ +package com.miller198.audiovisualizer.configs + +import android.media.audiofx.Visualizer +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.miller198.audiovisualizer.defaultPreProcessFftData + +/** + * Interface for configuring the audio visualizer. + * Supports custom, FFT, and waveform capture configurations. + */ +sealed interface VisualizerConfig { + + /** Whether to use waveform (time domain) capture */ + val useWaveCapture: Boolean + + /** Whether to use FFT (frequency domain) capture */ + val useFftCapture: Boolean + + /** Buffer size for audio capture (e.g., 512, 1024) */ + val captureSize: Int + + /** Optional FFT data processing function */ + val processFftData: ((Visualizer, ByteArray, Int) -> List)? + + /** Optional waveform data processing function */ + val processWaveData: ((Visualizer, ByteArray, Int) -> List)? + + /** Fully custom configuration defined by the user. */ + data class Custom( + override val useWaveCapture: Boolean, + override val useFftCapture: Boolean, + override val captureSize: Int, + override val processFftData: ((Visualizer, ByteArray, Int) -> List)?, + override val processWaveData: ((Visualizer, ByteArray, Int) -> List)? + ) : VisualizerConfig + + /** FFT-based visualizer configuration. */ + data class FftCaptureConfig( + val minFrequency: Int, // Minimum frequency to analyze (Hz) + val maxFrequency: Int, // Maximum frequency to analyze (Hz) + override val captureSize: Int, + override val processFftData: (Visualizer, ByteArray, Int) -> List, + ) : VisualizerConfig { + override val useWaveCapture: Boolean = false + override val useFftCapture: Boolean = true + override val processWaveData: ((Visualizer, ByteArray, Int) -> List)? = null + + init { + require(minFrequency >= 0 && maxFrequency >= 0) { + throw IllegalArgumentException("Minimum and maximum frequencies must be non-negative.") + } + require(minFrequency <= maxFrequency) { + throw IllegalArgumentException("Minimum frequency must be less than or equal to maximum frequency.") + } + } + + /** Default FFT configuration. */ + companion object { + const val DEFAULT_MIN_FREQ = 40 + const val DEFAULT_MAX_FREQ = 4000 + const val DEFAULT_CAPTURE_SIZE = 1024 + + val Default = FftCaptureConfig( + minFrequency = DEFAULT_MIN_FREQ, + maxFrequency = DEFAULT_MAX_FREQ, + captureSize = DEFAULT_CAPTURE_SIZE, + processFftData = { _, byteArray, samplingRate -> + // Default FFT preprocessing (custom implementation required) + defaultPreProcessFftData(byteArray, DEFAULT_CAPTURE_SIZE, DEFAULT_MIN_FREQ, DEFAULT_MAX_FREQ, samplingRate) + } + ) + } + } + + /** + * Waveform-based visualizer configuration. + */ + open class WaveCaptureConfig( + override val captureSize: Int, + override val processWaveData: (Visualizer, ByteArray, Int) -> List + ) : VisualizerConfig { + override val useWaveCapture: Boolean = true + override val useFftCapture: Boolean = false + override val processFftData: ((Visualizer, ByteArray, Int) -> List)? = null + + /** Default waveform configuration. */ + data object Default : WaveCaptureConfig( + captureSize = 1024, + processWaveData = { _, _, _ -> + // TODO: Implement default waveform preprocessing logic + emptyList() + } + ) + } +} + +/** + * Interface for gradient animation configuration. + */ +sealed interface GradientConfig { + val useGradient: Boolean // Whether gradient is enabled + val duration: Int // Animation duration in milliseconds + val color: Color // Base color of the gradient + + /** Default gradient config (enabled). */ + data object Default : GradientConfig { + override val useGradient: Boolean = true + override val duration: Int = 2500 + override val color: Color = White + } + + /** Enabled gradient config with custom options. */ + data class Enabled( + override val duration: Int, + override val color: Color, + ) : GradientConfig { + override val useGradient: Boolean = true + } + + /** Disabled gradient config (no animation, transparent color) */ + data object Disabled : GradientConfig { + override val useGradient: Boolean = false + override val duration: Int = 0 + override val color: Color = Color.Transparent + } +} + +/** + * Represents configuration options for determining the inner clipping radius of a visual element + * (such as the inner circle in an audio visualization). + * + * Implementations specify either a fixed radius in Dp, a relative ratio of the canvas radius, + * or a default value. + * + * @property dp The fixed radius in density-independent pixels (Dp). If greater than 0, this takes priority over [ratio]. + * @property ratio A relative ratio (0.0 to 1.0) used when [dp] is zero. Represents a proportion of the canvas radius. + */ +sealed interface ClippingRadiusConfig { + val dp: Dp + val ratio: Float + + /** + * Fixed radius configuration using a specific dp value. + * If this is used, the ratio is ignored. + * + * @param dp Must be >= 0.0 + */ + data class Fixed(override val dp: Dp) : ClippingRadiusConfig { + override val ratio: Float = 0f + + init { + require(dp.value >= 0f) { + throw IllegalArgumentException("dp must be >= 0, but was ${dp.value}") + } + } + } + + /** + * Ratio-based radius configuration using a value between 0.0 and 1.0. + * Represents a percentage of the canvas radius. + * + * @param ratio Must be in the range [0.0, 1.0] + */ + data class Ratio(override val ratio: Float) : ClippingRadiusConfig { + override val dp: Dp = 0.dp + + init { + require(ratio in 0f..1f) { + throw IllegalArgumentException("ratio must be in the range [0, 1], but was $ratio") + } + } + } + + /** + * Default configuration: ratio = 1.0 (full canvas radius), dp = 0.dp. + * This means inner clipping is applied maximally. + */ + data object FullClip : ClippingRadiusConfig { + override val dp: Dp = 0.dp + override val ratio: Float = 1f + } + + /** + * No clipping applied: ratio = 0.0, dp = 0.dp. + * The content starts from the edge of the canvas, without an inner gap. + */ + data object NoClip : ClippingRadiusConfig { + override val dp: Dp = 0.dp + override val ratio: Float = 0f + } + + /** + * Small inner clipping applied: ratio = 0.3, dp = 0.dp. + * Leaves a small circular gap in the center. + */ + data object Small : ClippingRadiusConfig { + override val dp: Dp = 0.dp + override val ratio: Float = 0.3f + } + + /** + * Medium inner clipping applied: ratio = 0.7, dp = 0.dp. + * Leaves a medium-sized circular gap in the center. + */ + data object Medium : ClippingRadiusConfig { + override val dp: Dp = 0.dp + override val ratio: Float = 0.7f + } +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/configs/VisualizerCallbacks.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/configs/VisualizerCallbacks.kt new file mode 100644 index 00000000..2c137d2d --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/configs/VisualizerCallbacks.kt @@ -0,0 +1,30 @@ +package com.miller198.audiovisualizer.configs + +import android.media.audiofx.Visualizer + +/** + * A container for user-defined callback functions for [Visualizer.OnDataCaptureListener]. + * + * @param onWaveCaptured Callback invoked when waveform (raw audio) data is captured. + * @param onFftCaptured Callback invoked when FFT (frequency domain) data is captured. + */ +data class VisualizerCallbacks( + val onWaveCaptured: (Visualizer, ByteArray, Int) -> Unit = { _, _, _ -> }, + val onFftCaptured: (Visualizer, ByteArray, Int) -> Unit = { _, _, _ -> }, +) { + /** + * Provides an implementation of [Visualizer.OnDataCaptureListener] that delegates + * waveform and FFT capture events to the corresponding user-defined callbacks. + * + * @return An instance of [Visualizer.OnDataCaptureListener] to be used with the Visualizer. + */ + fun provideVisualizerCallbacks() = object : Visualizer.OnDataCaptureListener { + override fun onWaveFormDataCapture(visualizer: Visualizer, bytes: ByteArray, samplingRate: Int) { + onWaveCaptured(visualizer, bytes, samplingRate) + } + + override fun onFftDataCapture(visualizer: Visualizer, bytes: ByteArray, samplingRate: Int) { + onFftCaptured(visualizer, bytes, samplingRate) + } + } +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundBar.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundBar.kt new file mode 100644 index 00000000..13b28ee0 --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundBar.kt @@ -0,0 +1,112 @@ +package com.miller198.audiovisualizer.soundeffect + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import com.miller198.audiovisualizer.soundeffect.DrawSoundEffectConfigs.GRADIENT_RADIUS_RATIO +import com.miller198.audiovisualizer.soundeffect.DrawSoundEffectConfigs.animatedGradientRadius +import com.miller198.audiovisualizer.soundeffect.DrawSoundEffectConfigs.gradientConfig +import com.miller198.audiovisualizer.soundeffect.DrawSoundEffectConfigs.onCanvasSizeChanged + +/** + * Composable function that draws a radial sound bar visualizer effect. + * + * Each audio data point is visualized as a bar (line) extending outward from the center + * in a circular pattern, with height proportional to the audio magnitude. + * + * @param audioData The list of normalized audio magnitudes (range: 0.0f to 1.0f). + * @param color The primary color used to draw the bars. + * @param modifier Modifier to apply to the Canvas layout. + */ +@Composable +internal fun DrawSoundBar( + audioData: List, + color: Color, + modifier: Modifier = Modifier, +) { + /** The radius from the center to the start of the bars */ + var adjustedRadius by remember { mutableFloatStateOf(0f) } + + /** The maximum possible bar height based on canvas size */ + var maxEffectHeight by remember { mutableFloatStateOf(0f) } + + /** Animated gradient radius for dynamic glow effects */ + val animatedGradientRadius = animatedGradientRadius(LinearEasing) + + /** Angle between each bar in the 360° circle */ + val angleStep = 360f / audioData.size + + /** Main canvas for drawing the sound bars */ + Canvas( + modifier = modifier + .fillMaxSize() + .onSizeChanged { canvasSize -> + // Recalculate radius and effect height when the canvas size changes + onCanvasSizeChanged( + width = canvasSize.width, + height = canvasSize.height, + onRadiusCalculated = { adjustedRadius = it }, + onMaxEffectHeightCalculated = { maxEffectHeight = it } + ) + } + ) { + val width = size.width + val height = size.height + + // Draw each audio data bar + audioData.forEachIndexed { idx, magnitude -> + val angle = getAudioDataAngle(idx, angleStep) + val barHeight = maxEffectHeight * magnitude + + // Starting point of the bar (on the inner radius) + val startOffset = getOffset( + centerX = width / 2, + centerY = height / 2, + angle = angle, + innerRadius = adjustedRadius, + extraLength = 0f + ) + // End point of the bar (based on magnitude) + val endOffset = getOffset( + centerX = width / 2, + centerY = height / 2, + angle = angle, + innerRadius = adjustedRadius, + extraLength = barHeight + ) + + // Draw the line with optional radial gradient + if (gradientConfig.useGradient) { + drawLine( + brush = Brush.radialGradient( + colors = listOf( + gradientConfig.color, + color + ), + center = startOffset, + radius = animatedGradientRadius * maxEffectHeight * GRADIENT_RADIUS_RATIO, + ), + start = startOffset, + end = endOffset, + strokeWidth = DrawSoundBarConstants.STROKE_WIDTH + ) + } else { + drawLine( + color = color, + start = startOffset, + end = endOffset, + strokeWidth = DrawSoundBarConstants.STROKE_WIDTH + ) + } + } + } +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundEffectConfigs.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundEffectConfigs.kt new file mode 100644 index 00000000..6e67b3aa --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundEffectConfigs.kt @@ -0,0 +1,99 @@ +package com.miller198.audiovisualizer.soundeffect + +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import com.miller198.audiovisualizer.configs.ClippingRadiusConfig +import com.miller198.audiovisualizer.configs.GradientConfig +import kotlin.math.min + +/** + * Configuration and utility object for controlling the sound effect drawing behavior. + */ +object DrawSoundEffectConfigs { + + /** Ratio used to calculate the gradient radius relative to the maximum effect radius. */ + const val GRADIENT_RADIUS_RATIO = 0.8f + + /** Divisor used to determine the maximum height of the visual wave effect. */ + const val EFFECT_HEIGHT_DIVISOR = 5f + + /** Current gradient configuration. Default is [GradientConfig.Default] */ + internal var gradientConfig: GradientConfig = GradientConfig.Default + + /** + * Current clipping radius configuration. Controls the inner radius of the sound wave. + * Default is [ClippingRadiusConfig.FullClip] + */ + internal var clippingRadiusConfig: ClippingRadiusConfig = ClippingRadiusConfig.FullClip + + /** + * Animated radius value for a radial gradient brush. + * Only animates if [GradientConfig.useGradient] of [gradientConfig] is true. + * + * @param easing Easing function used in the animation. + * @return Current animated radius value. + */ + val animatedGradientRadius: @Composable (Easing) -> Float = { easing -> + if (gradientConfig.useGradient) { + val transition = rememberInfiniteTransition(label = "GradientRadiusTransition") + transition.animateFloat( + initialValue = 0.01f, + targetValue = 1.0f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = gradientConfig.duration, easing = easing), + repeatMode = RepeatMode.Reverse + ), + label = "AnimatedGradientRadius" + ).value + } else { + 1f + } + } + + /** + * Called when the canvas size changes to update core dimensions like inner radius and maximum effect height. + * + * @param width New width of the canvas. + * @param height New height of the canvas. + * @param onRadiusCalculated Callback to provide the computed inner radius. + * @param onMaxEffectHeightCalculated Callback to provide the computed max wave height. + */ + internal fun onCanvasSizeChanged( + width: Int, + height: Int, + onRadiusCalculated: (Float) -> Unit, + onMaxEffectHeightCalculated: (Float) -> Unit + ) { + val clippingRadius = clippingRadiusConfig.dp.value + val clippingRadiusRatio = clippingRadiusConfig.ratio + + onRadiusCalculated( + if (clippingRadius > 0f) clippingRadius else (min(width, height) / 2) * clippingRadiusRatio + ) + + onMaxEffectHeightCalculated( + min(width, height) / EFFECT_HEIGHT_DIVISOR + ) + } +} + +/** + * Constants related to drawing the sound bars (e.g., circular bars). + */ +internal object DrawSoundBarConstants { + /** Default stroke width for drawing sound bars. */ + const val STROKE_WIDTH = 25f +} + +/** + * Constants related to drawing stroke-style sound waves. + */ +internal object DrawSoundWaveStrokeConstants { + /** Default stroke width for drawing wave outlines. */ + const val STROKE_WIDTH = 6f +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundEffectUtils.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundEffectUtils.kt new file mode 100644 index 00000000..88a4e7f8 --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundEffectUtils.kt @@ -0,0 +1,90 @@ +package com.miller198.audiovisualizer.soundeffect + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Path +import kotlin.math.cos +import kotlin.math.sin + +private val DEFAULT_OFFSET_ANGLE = Math.toRadians(-90.0) + +/** + * Converts an audio data index to an angle (in radians) for circular visualizations. + * + * @param index The index of the audio data. + * @param angleStep The step size between each index in degrees. + * @param offset Optional angle offset in radians. Defaults to top. + * @return The angle in radians as a [Float]. + */ +internal fun getAudioDataAngle( + index: Int, + angleStep: Float, + offset: Double = DEFAULT_OFFSET_ANGLE +): Float = (offset + Math.toRadians((index * angleStep).toDouble())).toFloat() + +/** + * Computes the screen position ([Offset]) for a point on a circle or arc. + * + * @param centerX The X coordinate of the center. + * @param centerY The Y coordinate of the center. + * @param angle The angle in radians. + * @param innerRadius The base radius from the center. + * @param extraLength Additional radial offset (e.g. for waveform height). + * @return The [Offset] position on the canvas. + */ +internal fun getOffset( + centerX: Float, + centerY: Float, + angle: Float, + innerRadius: Float, + extraLength: Float, +): Offset { + return Offset( + (centerX + (innerRadius + extraLength) * cos(angle)), + (centerY + (innerRadius + extraLength) * sin(angle)) + ) +} + +/** + * Draws a smooth, closed curve through a given list of [points] using the Catmull-Rom spline algorithm. + * + * This extension function generates a smooth path by interpolating between points using a Catmull-Rom spline. + * The path automatically wraps around to create a closed loop, making it useful for circular or looping shapes. + * + * @param points The list of [Offset] points to interpolate. + * @param steps The number of interpolation steps between each pair of points. Higher values create smoother curves. + */ +internal fun Path.catmullRomSpline(points: List, steps: Int = 10) { + if (points.size < 2) return + + val paddedPoints = listOf(points.last()) + points + listOf(points.first(), points[1]) + + for (i in 0 until paddedPoints.size - 3) { + val p0 = paddedPoints[i] + val p1 = paddedPoints[i + 1] + val p2 = paddedPoints[i + 2] + val p3 = paddedPoints[i + 3] + + for (t in 0..steps) { + val s = t / steps.toFloat() + val s2 = s * s + val s3 = s2 * s + + val x = 0.5f * ((2 * p1.x) + + (-p0.x + p2.x) * s + + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * s2 + + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * s3) + + val y = 0.5f * ((2 * p1.y) + + (-p0.y + p2.y) * s + + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * s2 + + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * s3) + + if (i == 0 && t == 0) { + moveTo(x, y) + } else { + lineTo(x, y) + } + } + } + close() +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundWaveFill.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundWaveFill.kt new file mode 100644 index 00000000..e40e87dc --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundWaveFill.kt @@ -0,0 +1,119 @@ +package com.miller198.audiovisualizer.soundeffect + +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.layout.onSizeChanged +import com.miller198.audiovisualizer.soundeffect.DrawSoundEffectConfigs.animatedGradientRadius +import com.miller198.audiovisualizer.soundeffect.DrawSoundEffectConfigs.gradientConfig +import com.miller198.audiovisualizer.soundeffect.DrawSoundEffectConfigs.onCanvasSizeChanged + +/** + * Composable function that draws a smooth radial waveform (filled area). + * + * This effect visualizes the waveform in a circular path using Catmull-Rom splines. + * + * @param audioData The list of normalized audio magnitudes (range: 0.0f to 1.0f). + * @param color The primary color used to fill the waveform. + * @param modifier Modifier to apply to the Canvas layout. + */ +@Composable +internal fun DrawSoundWaveFill( + audioData: List, + color: Color, + modifier: Modifier = Modifier, +) { + /** The radius from the center to the start of the bars */ + var adjustedRadius by remember { mutableFloatStateOf(0f) } + + /** The maximum possible bar height based on canvas size */ + var maxEffectHeight by remember { mutableFloatStateOf(0f) } + + /** Path used to define the circular center hole (to clip out) */ + var holePath by remember { mutableStateOf(Path()) } + + /** Angle between each bar in the 360° circle */ + val angleStep = 360f / audioData.size + + /** The path that represents the full waveform */ + val path = Path() + + /** Animated gradient radius for visual effect */ + val animatedGradientRadius = animatedGradientRadius(LinearOutSlowInEasing) + + /** Main canvas for drawing the sound wave */ + Canvas( + modifier = modifier + .fillMaxSize() + .onSizeChanged { canvasSize -> + // Recalculate radius and effect height when the canvas size changes + onCanvasSizeChanged( + width = canvasSize.width, + height = canvasSize.height, + onRadiusCalculated = { adjustedRadius = it }, + onMaxEffectHeightCalculated = { maxEffectHeight = it } + ) + holePath = holePath.apply { + addOval( + Rect( + center = Offset(x = canvasSize.width / 2f, y = canvasSize.height / 2f), + radius = adjustedRadius + 1f + ) + ) + } + } + ) { + val width = size.width + val height = size.height + + // Calculate wave points positioned radially + val points = audioData.mapIndexed { idx, magnitude -> + val angle = getAudioDataAngle(idx, angleStep) + val waveHeight = maxEffectHeight * magnitude + getOffset( + centerX = width / 2, + centerY = height / 2, + angle = angle, + innerRadius = adjustedRadius, + extraLength = waveHeight, + ) + } + + // Apply Catmull-Rom spline for smooth curve interpolation + path.catmullRomSpline(points) + + // Clip the central hole area and draw the wave around it + clipPath(holePath, clipOp = ClipOp.Difference) { + if (gradientConfig.useGradient) { + drawPath( + path = path, + brush = Brush.radialGradient( + colors = listOf(gradientConfig.color, color), + center = Offset(width / 2, height / 2), + radius = (adjustedRadius + maxEffectHeight).coerceAtLeast(0.01f) + * animatedGradientRadius + ) + ) + } else { + drawPath( + path = path, + color = color + ) + } + } + } +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundWaveStroke.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundWaveStroke.kt new file mode 100644 index 00000000..ac131a79 --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/DrawSoundWaveStroke.kt @@ -0,0 +1,83 @@ +package com.miller198.audiovisualizer.soundeffect + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.layout.onSizeChanged +import com.miller198.audiovisualizer.soundeffect.DrawSoundEffectConfigs.onCanvasSizeChanged + +/** + * Composable function that draws a smooth radial waveform (outline only). + * + * This effect visualizes the waveform in a circular path using Catmull-Rom splines. + * + * @param audioData The list of normalized audio magnitudes (range: 0.0f to 1.0f). + * @param color The primary color used to fill the waveform. + * @param modifier Modifier to apply to the Canvas layout. + */ +@Composable +internal fun DrawSoundWaveStroke( + audioData: List, + color: Color, + modifier: Modifier = Modifier, +) { + /** The radius from the center to the start of the bars */ + var adjustedRadius by remember { mutableFloatStateOf(0f) } + + /** The maximum possible bar height based on canvas size */ + var maxEffectHeight by remember { mutableFloatStateOf(0f) } + + /** Angle between each bar in the 360° circle */ + val angleStep = 360f / audioData.size + + /** The path that represents the full waveform */ + val path = Path() + + /** Main canvas for drawing the sound bars */ + Canvas( + modifier = modifier + .fillMaxSize() + .onSizeChanged { canvasSize -> + // Recalculate radius and effect height when the canvas size changes + onCanvasSizeChanged( + width = canvasSize.width, + height = canvasSize.height, + onRadiusCalculated = { adjustedRadius = it }, + onMaxEffectHeightCalculated = { maxEffectHeight = it } + ) + } + ) { + val width = size.width + val height = size.height + + // Calculate wave points positioned radially + val points = audioData.mapIndexed { idx, magnitude -> + val angle = getAudioDataAngle(idx, angleStep) + val waveHeight = maxEffectHeight * magnitude + getOffset( + centerX = width / 2, + centerY = height / 2, + angle = angle, + innerRadius = adjustedRadius, + extraLength = waveHeight, + ) + } + + // Apply Catmull-Rom spline for smooth curve interpolation + path.catmullRomSpline(points) + + drawPath( + path = path, + color = color, + style = Stroke(width = DrawSoundWaveStrokeConstants.STROKE_WIDTH) + ) + } +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/SoundEffects.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/SoundEffects.kt new file mode 100644 index 00000000..043a5410 --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/soundeffect/SoundEffects.kt @@ -0,0 +1,37 @@ +package com.miller198.audiovisualizer.soundeffect + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +/** + * Enum representing different sound visualization effects. + * Each effect defines a Composable function that draws the audio data using a specific visual style. + * + * @property drawEffect A Composable lambda that renders the corresponding sound effect. + */ +enum class SoundEffects( + val drawEffect: @Composable ( + audioData: List, + color: Color, + modifier: Modifier, + ) -> Unit +) { + /** No effect. This does not render any audio visualization. */ + NONE({ _, _, _ -> }), + + /** A vertical bar graph representation of the audio data. */ + BAR({ audioData, color, modifier -> + DrawSoundBar(audioData, color, modifier) + }), + + /** A waveform rendered using stroke (outline only). */ + WAVE_STROKE({ audioData, color, modifier -> + DrawSoundWaveStroke(audioData, color, modifier) + }), + + /** A waveform rendered as a filled shape. */ + WAVE_FILL({ audioData, color, modifier -> + DrawSoundWaveFill(audioData, color, modifier) + }) +} diff --git a/audio_visualizer/src/main/java/com/miller198/audiovisualizer/ui/CircleVisualizer.kt b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/ui/CircleVisualizer.kt new file mode 100644 index 00000000..58214934 --- /dev/null +++ b/audio_visualizer/src/main/java/com/miller198/audiovisualizer/ui/CircleVisualizer.kt @@ -0,0 +1,112 @@ +package com.miller198.audiovisualizer.ui + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.White +import com.miller198.audiovisualizer.BaseVisualizer +import com.miller198.audiovisualizer.configs.VisualizerConfig +import com.miller198.audiovisualizer.soundeffect.DrawSoundEffectConfigs +import com.miller198.audiovisualizer.configs.GradientConfig +import com.miller198.audiovisualizer.configs.ClippingRadiusConfig +import com.miller198.audiovisualizer.configs.VisualizerCallbacks +import com.miller198.audiovisualizer.soundeffect.SoundEffects +import kotlinx.coroutines.launch + +/** + * Composable that visualizes audio data as a circular sound effect (e.g., wave or bar), + * using a provided audio session ID. + * + * @param audioSessionId The audio session ID from the audio output source (e.g., MediaPlayer). + * @param soundEffects Object that defines how to draw the sound effect. use [SoundEffects] (waveform, bars, etc.). + * @param visualizerConfig Configuration for how the visualizer captures and processes audio data [VisualizerConfig]. + * @param modifier Modifier to apply to the visualizer's layout. + * @param color The primary color to use for drawing the sound visualization. + * @param clippingRadiusConfig Configuration for the inner clipping radius of the circle visualization. Default is [ClippingRadiusConfig.FullClip]. + * @param gradientConfig Configuration for gradient animation within the visualizer (optional). Default is [GradientConfig.Default] which uses gradient animation. + */ +@Composable +fun CircleVisualizer( + audioSessionId: Int, + soundEffects: SoundEffects, + visualizerConfig: VisualizerConfig, + modifier: Modifier = Modifier, + color: Color = White, + clippingRadiusConfig: ClippingRadiusConfig = ClippingRadiusConfig.FullClip, + gradientConfig: GradientConfig = GradientConfig.Default, +) { + /** Holds the current list of magnitude values */ + val magnitudes = remember { mutableStateOf>(emptyList()) } + + /** Animated version of the magnitudes for smooth transitions in visualization */ + val animateMagnitudes = remember { mutableStateOf>>(emptyList()) } + + val visualizer = remember { BaseVisualizer() } + + // Set global visual configuration (used in other rendering composable functions) + DrawSoundEffectConfigs.gradientConfig = gradientConfig + DrawSoundEffectConfigs.clippingRadiusConfig = clippingRadiusConfig + + // Start the visualizer when the composable is composed with the given audio session ID + LaunchedEffect(audioSessionId) { + visualizer.start( + audioSessionId = audioSessionId, + captureSize = visualizerConfig.captureSize, + useWaveCapture = visualizerConfig.useWaveCapture, + useFftCapture = visualizerConfig.useFftCapture, + visualizerCallbacks = VisualizerCallbacks( + // Callback for waveform audio data (optional processing) + onWaveCaptured = { visualizer, bytes, samplingRate -> + magnitudes.value = visualizerConfig.processWaveData?.invoke(visualizer, bytes, samplingRate) ?: emptyList() + }, + // Callback for FFT audio data (optional processing) + onFftCaptured = { visualizer, bytes, samplingRate -> + magnitudes.value = visualizerConfig.processFftData?.invoke(visualizer, bytes, samplingRate) ?: emptyList() + }, + ) + ) + } + + // Animate changes in magnitude values for smoother rendering transitions + LaunchedEffect(magnitudes.value) { + if (animateMagnitudes.value.isEmpty()) { + // Initialize the animatable list if not already done + animateMagnitudes.value = magnitudes.value.map { Animatable(it) } + } else { + // Animate each value to its new magnitude + magnitudes.value.forEachIndexed { i, magnitude -> + launch { + animateMagnitudes.value[i].animateTo( + targetValue = magnitude, + animationSpec = tween( + durationMillis = 120, + easing = FastOutSlowInEasing + ) + ) + } + } + } + } + + // Release the visualizer when composable leaves the composition + DisposableEffect(audioSessionId) { + onDispose { + visualizer.stop() + } + } + + // Draw the sound effect using the provided drawEffect lambda + soundEffects.drawEffect.invoke( + animateMagnitudes.value.map { it.value }, + color, + modifier, + ) +} diff --git a/audio_visualizer/src/test/java/com/miller198/audiovisualizer/ExampleUnitTest.kt b/audio_visualizer/src/test/java/com/miller198/audiovisualizer/ExampleUnitTest.kt new file mode 100644 index 00000000..1f3676f6 --- /dev/null +++ b/audio_visualizer/src/test/java/com/miller198/audiovisualizer/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.miller198.audiovisualizer + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/domain/.gitignore b/build-logic/convention/.gitignore similarity index 100% rename from domain/.gitignore rename to build-logic/convention/.gitignore diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts new file mode 100644 index 00000000..df21f6a0 --- /dev/null +++ b/build-logic/convention/build.gradle.kts @@ -0,0 +1,56 @@ +plugins { + `kotlin-dsl` +} + +group = "com.squirtles.build-logic" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + compileOnly(libs.android.gradlePlugin) + compileOnly(libs.android.tools.common) + compileOnly(libs.kotlin.gradlePlugin) + compileOnly(libs.ksp.gradlePlugin) +} + +gradlePlugin { + plugins { + register("androidApplication") { + id = "musicroad.android.application" + implementationClass = "com.squirtles.convention.AndroidApplicationPlugin" + } + + register("androidLibrary") { + id = "musicroad.android.library" + implementationClass = "com.squirtles.convention.AndroidLibraryPlugin" + } + + register("composeLibrary") { + id = "musicroad.compose.library" + implementationClass = "com.squirtles.convention.AndroidComposePlugin" + } + + register("javaLibrary") { + id = "musicroad.java.library" + implementationClass = "com.squirtles.convention.JavaLibraryPlugin" + } + + register("hilt") { + id = "musicroad.hilt" + implementationClass = "com.squirtles.convention.HiltPlugin" + } + + register("data") { + id = "musicroad.data" + implementationClass = "com.squirtles.convention.MusicRoadDataPlugin" + } + + register("feature") { + id = "musicroad.feature" + implementationClass = "com.squirtles.convention.MusicRoadFeaturePlugin" + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/AndroidApplicationPlugin.kt b/build-logic/convention/src/main/java/com/squirtles/convention/AndroidApplicationPlugin.kt new file mode 100644 index 00000000..f4ec51b9 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/AndroidApplicationPlugin.kt @@ -0,0 +1,34 @@ +package com.squirtles.convention + +import com.android.build.api.dsl.ApplicationExtension +import com.squirtles.convention.extensions.configureKotlinAndroid +import com.squirtles.convention.extensions.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +// app module +class AndroidApplicationPlugin: Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.run { + apply("com.android.application") + apply("org.jetbrains.kotlin.android") + } + + extensions.configure { + defaultConfig { + applicationId = "com.squirtles.musicroad" + + targetSdk = libs.findVersion("targetSdk").get().toString().toInt() + versionCode = libs.findVersion("versionCode").get().toString().toInt() + versionName = libs.findVersion("versionName").get().toString() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + configureKotlinAndroid(this) + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/AndroidComposePlugin.kt b/build-logic/convention/src/main/java/com/squirtles/convention/AndroidComposePlugin.kt new file mode 100644 index 00000000..444e2f55 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/AndroidComposePlugin.kt @@ -0,0 +1,27 @@ +package com.squirtles.convention + +import com.android.build.gradle.LibraryExtension +import com.squirtles.convention.extensions.configureComposeAndroid +import com.squirtles.convention.extensions.getLibrary +import com.squirtles.convention.extensions.implementation +import com.squirtles.convention.extensions.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +class AndroidComposePlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("musicroad.android.library") + + extensions.configure { + configureComposeAndroid(this) + } + + dependencies { + implementation(libs.getLibrary("kotlinx.immutable")) + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/AndroidLibraryPlugin.kt b/build-logic/convention/src/main/java/com/squirtles/convention/AndroidLibraryPlugin.kt new file mode 100644 index 00000000..9fc4303d --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/AndroidLibraryPlugin.kt @@ -0,0 +1,29 @@ +package com.squirtles.convention + +import com.android.build.gradle.LibraryExtension +import com.squirtles.convention.extensions.configureKotlinAndroid +import com.squirtles.convention.extensions.configureKotlinCoroutine +import com.squirtles.convention.extensions.getBundle +import com.squirtles.convention.extensions.implementation +import com.squirtles.convention.extensions.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +class AndroidLibraryPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("com.android.library") + + extensions.configure { + configureKotlinAndroid(this) + configureKotlinCoroutine(this) + } + + dependencies { + implementation(libs.getBundle("androidx-core")) + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/HiltPlugin.kt b/build-logic/convention/src/main/java/com/squirtles/convention/HiltPlugin.kt new file mode 100644 index 00000000..91c4b136 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/HiltPlugin.kt @@ -0,0 +1,29 @@ +package com.squirtles.convention + +import com.squirtles.convention.extensions.androidTestImplementation +import com.squirtles.convention.extensions.getLibrary +import com.squirtles.convention.extensions.implementation +import com.squirtles.convention.extensions.ksp +import com.squirtles.convention.extensions.kspTest +import com.squirtles.convention.extensions.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +class HiltPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.run { + apply("dagger.hilt.android.plugin") + apply("com.google.devtools.ksp") + } + + dependencies { + ksp(libs.getLibrary("hilt.android.compiler")) + kspTest(libs.getLibrary("hilt.android.compiler")) + implementation(libs.getLibrary("hilt.android")) + androidTestImplementation(libs.getLibrary("hilt.android.testing")) + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/JavaLibraryPlugin.kt b/build-logic/convention/src/main/java/com/squirtles/convention/JavaLibraryPlugin.kt new file mode 100644 index 00000000..5ec15d7a --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/JavaLibraryPlugin.kt @@ -0,0 +1,37 @@ +package com.squirtles.convention + +import com.squirtles.convention.extensions.getLibrary +import com.squirtles.convention.extensions.getVersion +import com.squirtles.convention.extensions.implementation +import com.squirtles.convention.extensions.libs +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension + +class JavaLibraryPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply { + apply("org.jetbrains.kotlin.jvm") + apply("java-library") + } + + extensions.configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + extensions.configure { + jvmToolchain(libs.getVersion("jdkVersion").requiredVersion.toInt()) + } + + dependencies { + implementation(libs.getLibrary("inject")) + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/MusicRoadDataPlugin.kt b/build-logic/convention/src/main/java/com/squirtles/convention/MusicRoadDataPlugin.kt new file mode 100644 index 00000000..ed1ee95a --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/MusicRoadDataPlugin.kt @@ -0,0 +1,22 @@ +package com.squirtles.convention + +import com.squirtles.convention.extensions.implementation +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +class MusicRoadDataPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply { + apply("musicroad.android.library") + apply("musicroad.hilt") + } + + dependencies { + implementation(project(":core:model")) + implementation(project(":core:buildconfig")) + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/MusicRoadFeaturePlugin.kt b/build-logic/convention/src/main/java/com/squirtles/convention/MusicRoadFeaturePlugin.kt new file mode 100644 index 00000000..7c8304f6 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/MusicRoadFeaturePlugin.kt @@ -0,0 +1,29 @@ +package com.squirtles.convention + +import com.squirtles.convention.extensions.getBundle +import com.squirtles.convention.extensions.implementation +import com.squirtles.convention.extensions.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + + +class MusicRoadFeaturePlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.run { + apply("musicroad.compose.library") + apply("musicroad.hilt") + } + + dependencies { + implementation(project(":core:model")) + implementation(project(":core:util")) + implementation(project(":core:common")) + implementation(project(":core:navigation")) + + implementation(libs.getBundle("navigation")) + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/extensions/ComposeAndroid.kt b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/ComposeAndroid.kt new file mode 100644 index 00000000..35498430 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/ComposeAndroid.kt @@ -0,0 +1,27 @@ +package com.squirtles.convention.extensions + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +internal fun Project.configureComposeAndroid(commonExtension: CommonExtension<*, *, *, *, *, *>) { + commonExtension.apply { + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = libs.getVersion("compose-compiler").requiredVersion + } + + dependencies { + val composeBom = libs.getLibrary("compose.bom") + implementation(platform(composeBom)) + androidTestImplementation(platform(composeBom)) + implementation(libs.getBundle("compose")) + implementation(libs.getBundle("material")) + debugImplementation(libs.getBundle("compose-debug")) + androidTestImplementation(libs.getBundle("compose-debug")) + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/extensions/DependencyHandlerScopeExtension.kt b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/DependencyHandlerScopeExtension.kt new file mode 100644 index 00000000..5421bc94 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/DependencyHandlerScopeExtension.kt @@ -0,0 +1,55 @@ +package com.squirtles.convention.extensions + +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.ConfigurableFileTree +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.DependencyHandlerScope + +fun DependencyHandlerScope.implementation(project: Project) { + "implementation"(project) +} + +fun DependencyHandlerScope.implementation(provider: Provider<*>) { + "implementation"(provider) +} + +fun DependencyHandlerScope.implementation(fileTree: ConfigurableFileTree) { + "implementation"(fileTree) +} + +fun DependencyHandlerScope.implementation(fileCollection: ConfigurableFileCollection) { + "implementation"(fileCollection) +} + +fun DependencyHandlerScope.debugImplementation(provider: Provider<*>) { + "debugImplementation"(provider) +} + +fun DependencyHandlerScope.releaseImplementation(provider: Provider<*>) { + "releaseImplementation"(provider) +} + +fun DependencyHandlerScope.ksp(provider: Provider<*>) { + "ksp"(provider) +} + +fun DependencyHandlerScope.kspTest(provider: Provider<*>) { + "kspTest"(provider) +} + +fun DependencyHandlerScope.coreLibraryDesugaring(provider: Provider<*>) { + "coreLibraryDesugaring"(provider) +} + +fun DependencyHandlerScope.androidTestImplementation(provider: Provider<*>) { + "androidTestImplementation"(provider) +} + +fun DependencyHandlerScope.testImplementation(provider: Provider<*>) { + "testImplementation"(provider) +} + +fun DependencyHandlerScope.compileOnly(provider: Provider<*>) { + "compileOnly"(provider) +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/extensions/KotlinAndroid.kt b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/KotlinAndroid.kt new file mode 100644 index 00000000..ebad7ec7 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/KotlinAndroid.kt @@ -0,0 +1,57 @@ +package com.squirtles.convention.extensions + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension<*, *, *, *, *, *>) { + pluginManager.apply("org.jetbrains.kotlin.android") + + commonExtension.apply { + compileSdk = libs.getVersion("compileSdk").requiredVersion.toInt() + + defaultConfig { + minSdk = libs.getVersion("minSdk").requiredVersion.toInt() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildTypes { + getByName("debug") { + proguardFiles( + getDefaultProguardFile("proguard-android.txt"), + "proguard-debug.pro", + ) + } + + getByName("release") { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro", + ) + } + } + + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + + freeCompilerArgs.addAll( + listOf( + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi", + ), + ) + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/extensions/KotlinCoroutine.kt b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/KotlinCoroutine.kt new file mode 100644 index 00000000..47024133 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/KotlinCoroutine.kt @@ -0,0 +1,13 @@ +package com.squirtles.convention.extensions + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +internal fun Project.configureKotlinCoroutine(commonExtension: CommonExtension<*, *, *, *, *, *>) { + commonExtension.apply { + dependencies { + implementation(libs.getBundle("coroutines")) + } + } +} diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/extensions/ProjectExtension.kt b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/ProjectExtension.kt new file mode 100644 index 00000000..b1c6e448 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/ProjectExtension.kt @@ -0,0 +1,9 @@ +package com.squirtles.convention.extensions + +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.getByType + +val Project.libs: VersionCatalog + get() = extensions.getByType().named("libs") diff --git a/build-logic/convention/src/main/java/com/squirtles/convention/extensions/VersionCatalogExtension.kt b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/VersionCatalogExtension.kt new file mode 100644 index 00000000..fd071610 --- /dev/null +++ b/build-logic/convention/src/main/java/com/squirtles/convention/extensions/VersionCatalogExtension.kt @@ -0,0 +1,22 @@ +package com.squirtles.convention.extensions + +import org.gradle.api.artifacts.ExternalModuleDependencyBundle +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionConstraint +import org.gradle.api.provider.Provider + +fun VersionCatalog.getBundle(bundleName: String): Provider = + findBundle(bundleName).orElseThrow { + NoSuchElementException("Bundle with name $bundleName not found in the catalog") + } + +fun VersionCatalog.getLibrary(libraryName: String): Provider = + findLibrary(libraryName).orElseThrow { + NoSuchElementException("Library with name $libraryName not found in the catalog") + } + +fun VersionCatalog.getVersion(versionName: String): VersionConstraint = + findVersion(versionName).orElseThrow { + NoSuchElementException("Version with name $versionName not found in the catalog") + } diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties new file mode 100644 index 00000000..c6cd2a7e --- /dev/null +++ b/build-logic/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configureondemand=true diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 00000000..c55e37ec --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,15 @@ +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" +include(":convention") diff --git a/build.gradle.kts b/build.gradle.kts index 4a8f4158..4610a785 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,4 +8,15 @@ plugins { alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.google.services) apply false alias(libs.plugins.firebase.crashlytics) apply false -} \ No newline at end of file + alias(libs.plugins.jetbrains.kotlin.jvm) apply false +} + +buildscript { + repositories { + google() + mavenCentral() + maven { + url = uri("https://repository.map.naver.com/archive/maven") + } + } +} diff --git a/mediaservice/.gitignore b/core/account/.gitignore similarity index 100% rename from mediaservice/.gitignore rename to core/account/.gitignore diff --git a/core/account/build.gradle.kts b/core/account/build.gradle.kts new file mode 100644 index 00000000..fb2d6ecc --- /dev/null +++ b/core/account/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.musicroad.android.library) + alias(libs.plugins.musicroad.hilt) +} + +android { + namespace = "com.squirtles.core.account" +} + +dependencies { + implementation(projects.domain.user) + implementation(projects.core.model) + implementation(projects.core.buildconfig) + + implementation(libs.bundles.auth) + implementation(libs.firebase.auth.ktx) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + // Credentials + implementation(libs.bundles.auth) +} diff --git a/domain/proguard-rules.pro b/core/account/proguard-rules.pro similarity index 100% rename from domain/proguard-rules.pro rename to core/account/proguard-rules.pro diff --git a/app/src/main/java/com/squirtles/musicroad/account/AccountViewModel.kt b/core/account/src/main/java/com/squirtles/core/account/AccountViewModel.kt similarity index 89% rename from app/src/main/java/com/squirtles/musicroad/account/AccountViewModel.kt rename to core/account/src/main/java/com/squirtles/core/account/AccountViewModel.kt index b0e3f734..c4e5fa23 100644 --- a/app/src/main/java/com/squirtles/musicroad/account/AccountViewModel.kt +++ b/core/account/src/main/java/com/squirtles/core/account/AccountViewModel.kt @@ -1,13 +1,13 @@ -package com.squirtles.musicroad.account +package com.squirtles.core.account import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential -import com.squirtles.domain.usecase.user.CreateGoogleIdUserUseCase -import com.squirtles.domain.usecase.user.DeleteAccountUseCase -import com.squirtles.domain.usecase.user.FetchUserByIdUseCase -import com.squirtles.domain.usecase.user.SignOutUseCase +import com.squirtles.domain.user.usecase.SignOutUseCase +import com.squirtles.domain.user.usecase.CreateGoogleIdUserUseCase +import com.squirtles.domain.user.usecase.DeleteAccountUseCase +import com.squirtles.domain.user.usecase.FetchUserByIdUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow diff --git a/app/src/main/java/com/squirtles/musicroad/account/GoogleId.kt b/core/account/src/main/java/com/squirtles/core/account/GoogleId.kt similarity index 96% rename from app/src/main/java/com/squirtles/musicroad/account/GoogleId.kt rename to core/account/src/main/java/com/squirtles/core/account/GoogleId.kt index 89922834..fc25b4d8 100644 --- a/app/src/main/java/com/squirtles/musicroad/account/GoogleId.kt +++ b/core/account/src/main/java/com/squirtles/core/account/GoogleId.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.account +package com.squirtles.core.account import android.content.Context import android.util.Log @@ -14,8 +14,7 @@ import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.GoogleAuthProvider -import com.squirtles.musicroad.BuildConfig -import com.squirtles.musicroad.R +import com.squirtles.core.buildconfig.LocalPropertyProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -28,7 +27,7 @@ class GoogleId(private val context: Context) { private val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() .setFilterByAuthorizedAccounts(false) - .setServerClientId(BuildConfig.GOOGLE_CLIENT_ID) + .setServerClientId(LocalPropertyProvider.googleClientId) .setAutoSelectEnabled(true) .build() diff --git a/core/account/src/main/res/values/strings.xml b/core/account/src/main/res/values/strings.xml new file mode 100644 index 00000000..9d28e755 --- /dev/null +++ b/core/account/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + + 기기에 로그인된 구글 계정이 없습니다 + Google 로그인을 사용할 수 없는 기기입니다 + 로그인에 실패했습니다 + diff --git a/core/buildconfig/.gitignore b/core/buildconfig/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/buildconfig/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/buildconfig/build.gradle.kts b/core/buildconfig/build.gradle.kts new file mode 100644 index 00000000..0402a087 --- /dev/null +++ b/core/buildconfig/build.gradle.kts @@ -0,0 +1,66 @@ +import java.io.FileInputStream +import java.util.Properties + +val properties = Properties().apply { + load(FileInputStream(rootProject.file("local.properties"))) +} + +plugins { + alias(libs.plugins.musicroad.android.library) +} + +android { + namespace = "com.squirtles.core.buildconfig" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigField( + "String", + "GOOGLE_CLIENT_ID", + "\"${properties.getProperty("GOOGLE_CLIENT_ID")}\"" + ) + + buildConfigField( + "String", + "APPLE_MUSIC_API_TOKEN", + "\"${properties.getProperty("APPLE_MUSIC_API_TOKEN")}\"" + ) + } + + buildTypes { + getByName("debug") { + isMinifyEnabled = false + + buildConfigField( + "String", + "FIRESTORE_DB_ID", + "\"${properties.getProperty("FIRESTORE_DB_ID_DEBUG")}\"" + ) + + buildConfigField( + "String", + "HTTPS_CALLABLE", + "\"${properties.getProperty("HTTPS_CALLABLE_DEBUG")}\"" + ) + } + + getByName("release") { + isMinifyEnabled = false + + buildConfigField( + "String", + "FIRESTORE_DB_ID", + "\"${properties.getProperty("FIRESTORE_DB_ID_RELEASE")}\"" + ) + + buildConfigField( + "String", + "HTTPS_CALLABLE", + "\"${properties.getProperty("HTTPS_CALLABLE_RELEASE")}\"" + ) + } + } +} diff --git a/core/buildconfig/src/main/java/com/squirtles/core/buildconfig/LocalPropertyProvider.kt b/core/buildconfig/src/main/java/com/squirtles/core/buildconfig/LocalPropertyProvider.kt new file mode 100644 index 00000000..7a6386b4 --- /dev/null +++ b/core/buildconfig/src/main/java/com/squirtles/core/buildconfig/LocalPropertyProvider.kt @@ -0,0 +1,15 @@ +package com.squirtles.core.buildconfig + +object LocalPropertyProvider { + val googleClientId: String + get() = BuildConfig.GOOGLE_CLIENT_ID + + val appleMusicApiToken: String + get() = BuildConfig.APPLE_MUSIC_API_TOKEN + + val firestoreDbId: String + get() = BuildConfig.FIRESTORE_DB_ID + + val httpsCallable: String + get() = BuildConfig.HTTPS_CALLABLE +} diff --git a/core/common/.gitignore b/core/common/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/common/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts new file mode 100644 index 00000000..5f530bcf --- /dev/null +++ b/core/common/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(libs.plugins.musicroad.compose.library) +} + +android { + namespace = "com.squirtles.core.common" +} + +dependencies { + + // Compose +// implementation(platform(libs.compose.bom)) +// implementation(libs.compose.runtime.android) +// implementation(libs.compose.ui.tooling.preview) +// implementation(platform(libs.compose.bom)) +// implementation(libs.bundles.compose) +// implementation(libs.bundles.compose.debug) +// implementation(libs.bundles.material) + + // Coil + implementation(libs.bundles.coil) +} diff --git a/mediaservice/proguard-rules.pro b/core/common/proguard-rules.pro similarity index 100% rename from mediaservice/proguard-rules.pro rename to core/common/proguard-rules.pro diff --git a/app/src/main/java/com/squirtles/musicroad/common/AlbumImage.kt b/core/common/src/main/java/com/squirtles/core/common/ui/AlbumImage.kt similarity index 90% rename from app/src/main/java/com/squirtles/musicroad/common/AlbumImage.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/AlbumImage.kt index 9dc6df16..8bb61b3f 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/AlbumImage.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/AlbumImage.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common +package com.squirtles.core.common.ui import android.util.Size import androidx.compose.runtime.Composable @@ -10,8 +10,8 @@ import androidx.compose.ui.res.stringResource import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.Gray +import com.squirtles.core.common.R +import com.squirtles.core.common.ui.theme.Gray @Composable fun AlbumImage( diff --git a/app/src/main/java/com/squirtles/musicroad/common/Constants.kt b/core/common/src/main/java/com/squirtles/core/common/ui/Constants.kt similarity index 58% rename from app/src/main/java/com/squirtles/musicroad/common/Constants.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/Constants.kt index 0ec41c4a..9d8fe320 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/Constants.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/Constants.kt @@ -1,11 +1,11 @@ -package com.squirtles.musicroad.common +package com.squirtles.core.common.ui import android.util.Size import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.Primary +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.Primary -internal object Constants { +object Constants { val DEFAULT_PADDING = 16.dp val REQUEST_IMAGE_SIZE_DEFAULT = Size(300, 300) @@ -14,4 +14,4 @@ internal object Constants { 0.0f to Primary, 0.25f to Black ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/squirtles/musicroad/common/CreatedByPickText.kt b/core/common/src/main/java/com/squirtles/core/common/ui/CreatedByPickText.kt similarity index 96% rename from app/src/main/java/com/squirtles/musicroad/common/CreatedByPickText.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/CreatedByPickText.kt index d9c6d39d..9fb60706 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/CreatedByPickText.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/CreatedByPickText.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common +package com.squirtles.core.common.ui import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -12,7 +12,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle -import com.squirtles.musicroad.R +import com.squirtles.core.common.R @Composable fun CreatedBySelfText( diff --git a/app/src/main/java/com/squirtles/musicroad/common/DefaultTopAppBar.kt b/core/common/src/main/java/com/squirtles/core/common/ui/DefaultTopAppBar.kt similarity index 94% rename from app/src/main/java/com/squirtles/musicroad/common/DefaultTopAppBar.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/DefaultTopAppBar.kt index 4dd71ca5..c1ae501b 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/DefaultTopAppBar.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/DefaultTopAppBar.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common +package com.squirtles.core.common.ui import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.displayCutoutPadding @@ -17,8 +17,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.R +import com.squirtles.core.common.ui.theme.White @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/squirtles/musicroad/common/MessageAlertDialog.kt b/core/common/src/main/java/com/squirtles/core/common/ui/MessageAlertDialog.kt similarity index 90% rename from app/src/main/java/com/squirtles/musicroad/common/MessageAlertDialog.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/MessageAlertDialog.kt index b49c6a3c..38d2f86f 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/MessageAlertDialog.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/MessageAlertDialog.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common +package com.squirtles.core.common.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -22,15 +22,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.MusicRoadTheme -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.R +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun MessageAlertDialog( +fun MessageAlertDialog( title: String, body: String, onDismissRequest: () -> Unit, @@ -80,7 +80,7 @@ internal fun MessageAlertDialog( } @Composable -internal fun DialogTextButton( +fun DialogTextButton( onClick: () -> Unit, text: String, textColor: Color = Black, @@ -103,7 +103,7 @@ internal fun DialogTextButton( @Preview(showBackground = true) @Composable -private fun DeletePickDialogPreview() { +fun DeletePickDialogPreview() { MusicRoadTheme { MessageAlertDialog( onDismissRequest = {}, diff --git a/app/src/main/java/com/squirtles/musicroad/common/PickInfoText.kt b/core/common/src/main/java/com/squirtles/core/common/ui/PickInfoText.kt similarity index 95% rename from app/src/main/java/com/squirtles/musicroad/common/PickInfoText.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/PickInfoText.kt index 8d40d760..82b8d2a5 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/PickInfoText.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/PickInfoText.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common +package com.squirtles.core.common.ui import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -14,8 +14,8 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.Primary +import com.squirtles.core.common.R +import com.squirtles.core.common.ui.theme.Primary @Composable fun SongInfoText( diff --git a/app/src/main/java/com/squirtles/musicroad/common/SignInAlertDialog.kt b/core/common/src/main/java/com/squirtles/core/common/ui/SignInAlertDialog.kt similarity index 89% rename from app/src/main/java/com/squirtles/musicroad/common/SignInAlertDialog.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/SignInAlertDialog.kt index 7141b4df..efe2ee68 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/SignInAlertDialog.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/SignInAlertDialog.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common +package com.squirtles.core.common.ui import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.BorderStroke @@ -30,18 +30,19 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.DarkGray -import com.squirtles.musicroad.ui.theme.MusicRoadTheme -import com.squirtles.musicroad.ui.theme.SignInButtonDarkBackground -import com.squirtles.musicroad.ui.theme.SignInButtonDarkStroke -import com.squirtles.musicroad.ui.theme.SignInButtonLightStroke -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.R +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.DarkGray +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.SignInButtonDarkBackground +import com.squirtles.core.common.ui.theme.SignInButtonDarkStroke +import com.squirtles.core.common.ui.theme.SignInButtonLightStroke +import com.squirtles.core.common.ui.theme.White + @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun SignInAlertDialog( +fun SignInAlertDialog( onDismissRequest: () -> Unit, onGoogleSignInClick: () -> Unit, description: String diff --git a/app/src/main/java/com/squirtles/musicroad/common/Spacer.kt b/core/common/src/main/java/com/squirtles/core/common/ui/Spacer.kt similarity index 91% rename from app/src/main/java/com/squirtles/musicroad/common/Spacer.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/Spacer.kt index 645717bb..c88c8080 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/Spacer.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/Spacer.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common +package com.squirtles.core.common.ui import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height diff --git a/app/src/main/java/com/squirtles/musicroad/ui/theme/Color.kt b/core/common/src/main/java/com/squirtles/core/common/ui/theme/Color.kt similarity index 93% rename from app/src/main/java/com/squirtles/musicroad/ui/theme/Color.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/theme/Color.kt index 6ed2ce1a..86f4a3bd 100644 --- a/app/src/main/java/com/squirtles/musicroad/ui/theme/Color.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.ui.theme +package com.squirtles.core.common.ui.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/com/squirtles/musicroad/ui/theme/Theme.kt b/core/common/src/main/java/com/squirtles/core/common/ui/theme/Theme.kt similarity index 96% rename from app/src/main/java/com/squirtles/musicroad/ui/theme/Theme.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/theme/Theme.kt index dc6797f4..b091b27a 100644 --- a/app/src/main/java/com/squirtles/musicroad/ui/theme/Theme.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.ui.theme +package com.squirtles.core.common.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -42,4 +42,4 @@ fun MusicRoadTheme( typography = Typography, content = content ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/squirtles/musicroad/ui/theme/Type.kt b/core/common/src/main/java/com/squirtles/core/common/ui/theme/Type.kt similarity index 95% rename from app/src/main/java/com/squirtles/musicroad/ui/theme/Type.kt rename to core/common/src/main/java/com/squirtles/core/common/ui/theme/Type.kt index 24437693..bcbff2be 100644 --- a/app/src/main/java/com/squirtles/musicroad/ui/theme/Type.kt +++ b/core/common/src/main/java/com/squirtles/core/common/ui/theme/Type.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.ui.theme +package com.squirtles.core.common.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle @@ -31,4 +31,4 @@ val Typography = Typography( letterSpacing = 0.5.sp ) */ -) \ No newline at end of file +) diff --git a/app/src/main/res/drawable/ic_favorite.xml b/core/common/src/main/res/drawable/ic_favorite.xml similarity index 100% rename from app/src/main/res/drawable/ic_favorite.xml rename to core/common/src/main/res/drawable/ic_favorite.xml diff --git a/app/src/main/res/drawable/img_google_logo.png b/core/common/src/main/res/drawable/img_google_logo.png similarity index 100% rename from app/src/main/res/drawable/img_google_logo.png rename to core/common/src/main/res/drawable/img_google_logo.png diff --git a/core/common/src/main/res/values/strings.xml b/core/common/src/main/res/values/strings.xml new file mode 100644 index 00000000..eae4d2ac --- /dev/null +++ b/core/common/src/main/res/values/strings.xml @@ -0,0 +1,30 @@ + + 앨범 이미지 + + 내가 + 등록한 픽 + 님의 픽 + + 상단 바 뒤로 가기 버튼 + + 삭제하시겠습니까? + 등록하신 픽이 삭제됩니다. + + 픽을 담은 개수 + 전체 + + + 로그인 + Sign in with Google + Google Logo + + + 더 많은 기능을 이용하기 위해\n로그인이 필요합니다 + 담은 픽을 확인하기 위해\n로그인이 필요합니다 + 픽을 등록하기 위해\n로그인이 필요합니다 + 픽을 담기 위해\n로그인이 필요합니다 + 픽을 담기 위해\n로그인이 필요합니다 + 로그인이 필요합니다 + 취소 + 기기에 로그인된 구글 계정이 없습니다 + diff --git a/core/mediaservice/.gitignore b/core/mediaservice/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/mediaservice/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/mediaservice/build.gradle.kts b/core/mediaservice/build.gradle.kts new file mode 100644 index 00000000..d69508cf --- /dev/null +++ b/core/mediaservice/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.musicroad.android.library) + alias(libs.plugins.musicroad.hilt) +} + +android { + namespace = "com.squirtles.core.mediaservice" +} + +dependencies { + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + // ExoPlayer + implementation(libs.bundles.exoplayer) + implementation(libs.androidx.media3.session) +} diff --git a/core/mediaservice/proguard-rules.pro b/core/mediaservice/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/mediaservice/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/mediaservice/src/androidTest/java/com/squirtles/mediaservice/ExampleInstrumentedTest.kt b/core/mediaservice/src/androidTest/java/com/squirtles/core/mediaservice/ExampleInstrumentedTest.kt similarity index 100% rename from mediaservice/src/androidTest/java/com/squirtles/mediaservice/ExampleInstrumentedTest.kt rename to core/mediaservice/src/androidTest/java/com/squirtles/core/mediaservice/ExampleInstrumentedTest.kt diff --git a/mediaservice/src/main/java/com/squirtles/mediaservice/CustomMediaSessionCallback.kt b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/CustomMediaSessionCallback.kt similarity index 98% rename from mediaservice/src/main/java/com/squirtles/mediaservice/CustomMediaSessionCallback.kt rename to core/mediaservice/src/main/java/com/squirtles/core/mediaservice/CustomMediaSessionCallback.kt index ac9ae731..b4f3f59c 100644 --- a/mediaservice/src/main/java/com/squirtles/mediaservice/CustomMediaSessionCallback.kt +++ b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/CustomMediaSessionCallback.kt @@ -1,4 +1,4 @@ -package com.squirtles.mediaservice +package com.squirtles.core.mediaservice import android.os.Build import android.os.Bundle diff --git a/mediaservice/src/main/java/com/squirtles/mediaservice/MediaControllerProvider.kt b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/MediaControllerProvider.kt similarity index 98% rename from mediaservice/src/main/java/com/squirtles/mediaservice/MediaControllerProvider.kt rename to core/mediaservice/src/main/java/com/squirtles/core/mediaservice/MediaControllerProvider.kt index b17d9b54..6d0be76e 100644 --- a/mediaservice/src/main/java/com/squirtles/mediaservice/MediaControllerProvider.kt +++ b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/MediaControllerProvider.kt @@ -1,4 +1,4 @@ -package com.squirtles.mediaservice +package com.squirtles.core.mediaservice import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaController diff --git a/mediaservice/src/main/java/com/squirtles/mediaservice/MediaNotificationProvider.kt b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/MediaNotificationProvider.kt similarity index 98% rename from mediaservice/src/main/java/com/squirtles/mediaservice/MediaNotificationProvider.kt rename to core/mediaservice/src/main/java/com/squirtles/core/mediaservice/MediaNotificationProvider.kt index ada84fe7..1d3e53f4 100644 --- a/mediaservice/src/main/java/com/squirtles/mediaservice/MediaNotificationProvider.kt +++ b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/MediaNotificationProvider.kt @@ -1,4 +1,4 @@ -package com.squirtles.mediaservice +package com.squirtles.core.mediaservice import android.annotation.SuppressLint import android.app.NotificationChannel @@ -16,6 +16,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession import androidx.media3.session.MediaStyleNotificationHelper +import com.squirtles.core.mediaservice.R import javax.inject.Inject interface MediaNotificationProvider { diff --git a/mediaservice/src/main/java/com/squirtles/mediaservice/MediaPlayerService.kt b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/MediaPlayerService.kt similarity index 98% rename from mediaservice/src/main/java/com/squirtles/mediaservice/MediaPlayerService.kt rename to core/mediaservice/src/main/java/com/squirtles/core/mediaservice/MediaPlayerService.kt index b5a028a4..3be6a7d7 100644 --- a/mediaservice/src/main/java/com/squirtles/mediaservice/MediaPlayerService.kt +++ b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/MediaPlayerService.kt @@ -1,4 +1,4 @@ -package com.squirtles.mediaservice +package com.squirtles.core.mediaservice import android.content.Intent import android.os.Bundle diff --git a/mediaservice/src/main/java/com/squirtles/mediaservice/PlayerCommands.kt b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/PlayerCommands.kt similarity index 97% rename from mediaservice/src/main/java/com/squirtles/mediaservice/PlayerCommands.kt rename to core/mediaservice/src/main/java/com/squirtles/core/mediaservice/PlayerCommands.kt index a26936d0..f8d8d91b 100644 --- a/mediaservice/src/main/java/com/squirtles/mediaservice/PlayerCommands.kt +++ b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/PlayerCommands.kt @@ -1,4 +1,4 @@ -package com.squirtles.mediaservice +package com.squirtles.core.mediaservice import android.os.Bundle import androidx.media3.session.SessionCommand diff --git a/mediaservice/src/main/java/com/squirtles/mediaservice/di/MediaDiModule.kt b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/di/MediaDiModule.kt similarity index 86% rename from mediaservice/src/main/java/com/squirtles/mediaservice/di/MediaDiModule.kt rename to core/mediaservice/src/main/java/com/squirtles/core/mediaservice/di/MediaDiModule.kt index 5d9d13a5..935e4deb 100644 --- a/mediaservice/src/main/java/com/squirtles/mediaservice/di/MediaDiModule.kt +++ b/core/mediaservice/src/main/java/com/squirtles/core/mediaservice/di/MediaDiModule.kt @@ -1,4 +1,4 @@ -package com.squirtles.mediaservice.di +package com.squirtles.core.mediaservice.di import android.content.ComponentName import android.content.Context @@ -9,13 +9,13 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaController import androidx.media3.session.MediaSession import androidx.media3.session.SessionToken +import com.squirtles.core.mediaservice.CustomMediaSessionCallback +import com.squirtles.core.mediaservice.MediaControllerProvider +import com.squirtles.core.mediaservice.MediaControllerProviderImpl +import com.squirtles.core.mediaservice.MediaNotificationProvider +import com.squirtles.core.mediaservice.MediaNotificationProviderImpl +import com.squirtles.core.mediaservice.MediaPlayerService import com.google.common.util.concurrent.ListenableFuture -import com.squirtles.mediaservice.CustomMediaSessionCallback -import com.squirtles.mediaservice.MediaControllerProvider -import com.squirtles.mediaservice.MediaControllerProviderImpl -import com.squirtles.mediaservice.MediaNotificationProvider -import com.squirtles.mediaservice.MediaNotificationProviderImpl -import com.squirtles.mediaservice.MediaPlayerService import dagger.Binds import dagger.Module import dagger.Provides diff --git a/app/src/main/res/drawable/ic_musicroad_foreground.xml b/core/mediaservice/src/main/res/drawable/ic_musicroad_foreground.xml similarity index 100% rename from app/src/main/res/drawable/ic_musicroad_foreground.xml rename to core/mediaservice/src/main/res/drawable/ic_musicroad_foreground.xml diff --git a/mediaservice/src/test/java/com/squirtles/mediaservice/ExampleUnitTest.kt b/core/mediaservice/src/test/java/com/squirtles/core/mediaservice/ExampleUnitTest.kt similarity index 100% rename from mediaservice/src/test/java/com/squirtles/mediaservice/ExampleUnitTest.kt rename to core/mediaservice/src/test/java/com/squirtles/core/mediaservice/ExampleUnitTest.kt diff --git a/core/model/.gitignore b/core/model/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/model/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts new file mode 100644 index 00000000..89e1c426 --- /dev/null +++ b/core/model/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(libs.plugins.musicroad.java.library) + alias(libs.plugins.kotlin.serialization) +} + +dependencies { + // Serialization + implementation(libs.kotlinx.serialization.json) +} diff --git a/core/model/proguard-rules.pro b/core/model/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/model/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/src/main/java/com/squirtles/domain/model/MusicVideo.kt b/core/model/src/main/java/com/squirtles/core/model/MusicVideo.kt similarity index 87% rename from domain/src/main/java/com/squirtles/domain/model/MusicVideo.kt rename to core/model/src/main/java/com/squirtles/core/model/MusicVideo.kt index e500fa1b..37e259bf 100644 --- a/domain/src/main/java/com/squirtles/domain/model/MusicVideo.kt +++ b/core/model/src/main/java/com/squirtles/core/model/MusicVideo.kt @@ -1,4 +1,4 @@ -package com.squirtles.domain.model +package com.squirtles.core.model import java.time.LocalDate diff --git a/domain/src/main/java/com/squirtles/domain/model/Order.kt b/core/model/src/main/java/com/squirtles/core/model/Order.kt similarity index 65% rename from domain/src/main/java/com/squirtles/domain/model/Order.kt rename to core/model/src/main/java/com/squirtles/core/model/Order.kt index b5120989..4b91e795 100644 --- a/domain/src/main/java/com/squirtles/domain/model/Order.kt +++ b/core/model/src/main/java/com/squirtles/core/model/Order.kt @@ -1,4 +1,4 @@ -package com.squirtles.domain.model +package com.squirtles.core.model enum class Order { LATEST, diff --git a/domain/src/main/java/com/squirtles/domain/model/Pick.kt b/core/model/src/main/java/com/squirtles/core/model/Pick.kt similarity index 89% rename from domain/src/main/java/com/squirtles/domain/model/Pick.kt rename to core/model/src/main/java/com/squirtles/core/model/Pick.kt index c6d3c225..12d6a1c1 100644 --- a/domain/src/main/java/com/squirtles/domain/model/Pick.kt +++ b/core/model/src/main/java/com/squirtles/core/model/Pick.kt @@ -1,4 +1,4 @@ -package com.squirtles.domain.model +package com.squirtles.core.model /** * 앱에서 사용하기 위한 Pick 정보 데이터클래스 @@ -24,5 +24,5 @@ data class LocationPoint( data class Creator( val uid: String, - val userName: String + val userName: String, ) diff --git a/domain/src/main/java/com/squirtles/domain/model/PlayerState.kt b/core/model/src/main/java/com/squirtles/core/model/PlayerState.kt similarity index 88% rename from domain/src/main/java/com/squirtles/domain/model/PlayerState.kt rename to core/model/src/main/java/com/squirtles/core/model/PlayerState.kt index ad1394d1..12a6bbee 100644 --- a/domain/src/main/java/com/squirtles/domain/model/PlayerState.kt +++ b/core/model/src/main/java/com/squirtles/core/model/PlayerState.kt @@ -1,4 +1,4 @@ -package com.squirtles.domain.model +package com.squirtles.core.model data class PlayerState( val id: String = "", diff --git a/domain/src/main/java/com/squirtles/domain/model/Song.kt b/core/model/src/main/java/com/squirtles/core/model/Song.kt similarity index 94% rename from domain/src/main/java/com/squirtles/domain/model/Song.kt rename to core/model/src/main/java/com/squirtles/core/model/Song.kt index b80007df..4d85cf66 100644 --- a/domain/src/main/java/com/squirtles/domain/model/Song.kt +++ b/core/model/src/main/java/com/squirtles/core/model/Song.kt @@ -1,4 +1,4 @@ -package com.squirtles.domain.model +package com.squirtles.core.model import kotlinx.serialization.Serializable diff --git a/domain/src/main/java/com/squirtles/domain/model/User.kt b/core/model/src/main/java/com/squirtles/core/model/User.kt similarity index 81% rename from domain/src/main/java/com/squirtles/domain/model/User.kt rename to core/model/src/main/java/com/squirtles/core/model/User.kt index 3f69249f..f493e263 100644 --- a/domain/src/main/java/com/squirtles/domain/model/User.kt +++ b/core/model/src/main/java/com/squirtles/core/model/User.kt @@ -1,4 +1,4 @@ -package com.squirtles.domain.model +package com.squirtles.core.model data class User( val uid: String, diff --git a/core/musicplayer/.gitignore b/core/musicplayer/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/musicplayer/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/musicplayer/build.gradle.kts b/core/musicplayer/build.gradle.kts new file mode 100644 index 00000000..70ba497d --- /dev/null +++ b/core/musicplayer/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.musicroad.android.library) + alias(libs.plugins.musicroad.hilt) +} + +android { + namespace = "com.squirtles.core.musicplayer" +} + +dependencies { + implementation(projects.domain.player) + implementation(projects.core.model) + implementation(libs.material) + + // ExoPlayer + implementation(libs.bundles.exoplayer) + implementation(libs.androidx.media3.session) +} diff --git a/core/musicplayer/proguard-rules.pro b/core/musicplayer/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/musicplayer/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/java/com/squirtles/musicroad/media/PlayerServiceViewModel.kt b/core/musicplayer/src/main/java/com/squirtles/core/musicplayer/PlayerServiceViewModel.kt similarity index 88% rename from app/src/main/java/com/squirtles/musicroad/media/PlayerServiceViewModel.kt rename to core/musicplayer/src/main/java/com/squirtles/core/musicplayer/PlayerServiceViewModel.kt index 769d92d7..93f6b4be 100644 --- a/app/src/main/java/com/squirtles/musicroad/media/PlayerServiceViewModel.kt +++ b/core/musicplayer/src/main/java/com/squirtles/core/musicplayer/PlayerServiceViewModel.kt @@ -1,12 +1,11 @@ -package com.squirtles.musicroad.media +package com.squirtles.core.musicplayer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.PlayerState -import com.squirtles.domain.model.Song -import com.squirtles.domain.usecase.player.MediaPlayerListenerUseCase -import com.squirtles.domain.usecase.player.MediaPlayerUseCase +import com.squirtles.core.model.Pick +import com.squirtles.core.model.PlayerState +import com.squirtles.domain.player.MediaPlayerListenerUseCase +import com.squirtles.domain.player.MediaPlayerUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.flow.SharingStarted @@ -87,7 +86,7 @@ class PlayerServiceViewModel @Inject constructor( } } - fun togglePlayPause(song: Song) { + fun togglePlayPause() { if (playerState.value.isPlaying) { onPause() } else { diff --git a/app/src/main/java/com/squirtles/musicroad/media/PlayerUiState.kt b/core/musicplayer/src/main/java/com/squirtles/core/musicplayer/PlayerUiState.kt similarity index 88% rename from app/src/main/java/com/squirtles/musicroad/media/PlayerUiState.kt rename to core/musicplayer/src/main/java/com/squirtles/core/musicplayer/PlayerUiState.kt index 41953fbf..826121bf 100644 --- a/app/src/main/java/com/squirtles/musicroad/media/PlayerUiState.kt +++ b/core/musicplayer/src/main/java/com/squirtles/core/musicplayer/PlayerUiState.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.media +package com.squirtles.core.musicplayer data class PlayerUiState( val isReady: Boolean = true, diff --git a/app/src/main/java/com/squirtles/musicroad/media/PlayerViewModel.kt b/core/musicplayer/src/main/java/com/squirtles/core/musicplayer/PlayerViewModel.kt similarity index 96% rename from app/src/main/java/com/squirtles/musicroad/media/PlayerViewModel.kt rename to core/musicplayer/src/main/java/com/squirtles/core/musicplayer/PlayerViewModel.kt index 617b1891..c7b83890 100644 --- a/app/src/main/java/com/squirtles/musicroad/media/PlayerViewModel.kt +++ b/core/musicplayer/src/main/java/com/squirtles/core/musicplayer/PlayerViewModel.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.media +package com.squirtles.core.musicplayer import android.content.Context import android.util.Log @@ -11,8 +11,8 @@ import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer -import com.squirtles.musicroad.media.PlayerUiState.Companion.PLAYER_STATE_INITIAL -import com.squirtles.musicroad.media.PlayerUiState.Companion.PLAYER_STATE_STOP +import com.squirtles.core.musicplayer.PlayerUiState.Companion.PLAYER_STATE_INITIAL +import com.squirtles.core.musicplayer.PlayerUiState.Companion.PLAYER_STATE_STOP import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow diff --git a/core/navigation/.gitignore b/core/navigation/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/navigation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts new file mode 100644 index 00000000..c7c44215 --- /dev/null +++ b/core/navigation/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(libs.plugins.musicroad.java.library) + alias(libs.plugins.kotlin.serialization) +} + +dependencies { + implementation(projects.core.model) + + // Serialization + implementation(libs.kotlinx.serialization.json) +} diff --git a/core/navigation/proguard-rules.pro b/core/navigation/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/navigation/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/java/com/squirtles/musicroad/navigation/MainRoute.kt b/core/navigation/src/main/java/com/squirtles/core/navigation/MainRoute.kt similarity index 87% rename from app/src/main/java/com/squirtles/musicroad/navigation/MainRoute.kt rename to core/navigation/src/main/java/com/squirtles/core/navigation/MainRoute.kt index 2d4c35dd..b285e60f 100644 --- a/app/src/main/java/com/squirtles/musicroad/navigation/MainRoute.kt +++ b/core/navigation/src/main/java/com/squirtles/core/navigation/MainRoute.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.navigation +package com.squirtles.core.navigation import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/squirtles/musicroad/navigation/MapRoute.kt b/core/navigation/src/main/java/com/squirtles/core/navigation/MapRoute.kt similarity index 79% rename from app/src/main/java/com/squirtles/musicroad/navigation/MapRoute.kt rename to core/navigation/src/main/java/com/squirtles/core/navigation/MapRoute.kt index 4a38424e..ec99b12b 100644 --- a/app/src/main/java/com/squirtles/musicroad/navigation/MapRoute.kt +++ b/core/navigation/src/main/java/com/squirtles/core/navigation/MapRoute.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.navigation +package com.squirtles.core.navigation import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/squirtles/musicroad/navigation/Route.kt b/core/navigation/src/main/java/com/squirtles/core/navigation/Route.kt similarity index 75% rename from app/src/main/java/com/squirtles/musicroad/navigation/Route.kt rename to core/navigation/src/main/java/com/squirtles/core/navigation/Route.kt index 7e7db78e..77ae775c 100644 --- a/app/src/main/java/com/squirtles/musicroad/navigation/Route.kt +++ b/core/navigation/src/main/java/com/squirtles/core/navigation/Route.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.navigation +package com.squirtles.core.navigation import kotlinx.serialization.Serializable diff --git a/core/navigation/src/main/java/com/squirtles/core/navigation/SearchRoute.kt b/core/navigation/src/main/java/com/squirtles/core/navigation/SearchRoute.kt new file mode 100644 index 00000000..8ec47bc3 --- /dev/null +++ b/core/navigation/src/main/java/com/squirtles/core/navigation/SearchRoute.kt @@ -0,0 +1,10 @@ +package com.squirtles.core.navigation + +import com.squirtles.core.model.Song +import kotlinx.serialization.Serializable + +@Serializable +sealed interface SearchRoute : Route { + @Serializable + data class Create(val song: Song) : SearchRoute +} diff --git a/app/src/main/java/com/squirtles/musicroad/navigation/UserInfoRoute.kt b/core/navigation/src/main/java/com/squirtles/core/navigation/UserInfoRoute.kt similarity index 88% rename from app/src/main/java/com/squirtles/musicroad/navigation/UserInfoRoute.kt rename to core/navigation/src/main/java/com/squirtles/core/navigation/UserInfoRoute.kt index b5a7d09f..d73dd1e7 100644 --- a/app/src/main/java/com/squirtles/musicroad/navigation/UserInfoRoute.kt +++ b/core/navigation/src/main/java/com/squirtles/core/navigation/UserInfoRoute.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.navigation +package com.squirtles.core.navigation import kotlinx.serialization.Serializable diff --git a/core/picklist/.gitignore b/core/picklist/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/picklist/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/picklist/build.gradle.kts b/core/picklist/build.gradle.kts new file mode 100644 index 00000000..03e0ac16 --- /dev/null +++ b/core/picklist/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(libs.plugins.musicroad.compose.library) +} + +android { + namespace = "com.squirtles.core.picklist" +} + +dependencies { + implementation(projects.core.model) + implementation(projects.core.common) + implementation(projects.domain.picklist) + implementation(projects.domain.user) + + implementation(libs.inject) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + // Coil + implementation(libs.bundles.coil) +} diff --git a/core/picklist/proguard-rules.pro b/core/picklist/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/picklist/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/picklist/src/androidTest/java/com/squirtles/core/picklist/ExampleInstrumentedTest.kt b/core/picklist/src/androidTest/java/com/squirtles/core/picklist/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..3fe06e20 --- /dev/null +++ b/core/picklist/src/androidTest/java/com/squirtles/core/picklist/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.squirtles.picklist + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.squirtles.picklist.test", appContext.packageName) + } +} diff --git a/app/src/main/java/com/squirtles/musicroad/common/picklist/PickListScreenContents.kt b/core/picklist/src/main/java/com/squirtles/core/picklist/PickListScreenContents.kt similarity index 91% rename from app/src/main/java/com/squirtles/musicroad/common/picklist/PickListScreenContents.kt rename to core/picklist/src/main/java/com/squirtles/core/picklist/PickListScreenContents.kt index c2a5ea1d..3696b8e4 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/picklist/PickListScreenContents.kt +++ b/core/picklist/src/main/java/com/squirtles/core/picklist/PickListScreenContents.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common.picklist +package com.squirtles.core.picklist import android.content.res.Configuration import androidx.compose.foundation.background @@ -33,21 +33,20 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.wear.compose.material.CircularProgressIndicator -import com.squirtles.domain.model.Order -import com.squirtles.domain.model.Pick -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.Constants.COLOR_STOPS -import com.squirtles.musicroad.common.Constants.DEFAULT_PADDING -import com.squirtles.musicroad.common.CountText -import com.squirtles.musicroad.common.DefaultTopAppBar -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.common.picklist.components.DeleteSelectedPickDialog -import com.squirtles.musicroad.common.picklist.components.EditModeAction -import com.squirtles.musicroad.common.picklist.components.EditModeBottomButton -import com.squirtles.musicroad.common.picklist.components.OrderBottomSheet -import com.squirtles.musicroad.common.picklist.components.PickItem -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.Constants.COLOR_STOPS +import com.squirtles.core.common.ui.Constants.DEFAULT_PADDING +import com.squirtles.core.common.ui.CountText +import com.squirtles.core.common.ui.DefaultTopAppBar +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.model.Order +import com.squirtles.core.model.Pick +import com.squirtles.core.picklist.components.DeleteSelectedPickDialog +import com.squirtles.core.picklist.components.EditModeAction +import com.squirtles.core.picklist.components.EditModeBottomButton +import com.squirtles.core.picklist.components.OrderBottomSheet +import com.squirtles.core.picklist.components.PickItem @Composable fun PickListScreenContents( @@ -94,7 +93,7 @@ fun PickListScreenContents( ), onBackClick = onBackClick, actions = { - if (getUid() == uid) { + if(getUid() == uid){ EditModeAction( isEditMode = isEditMode, enabled = uiState is PickListUiState.Success, diff --git a/app/src/main/java/com/squirtles/musicroad/common/picklist/PickListType.kt b/core/picklist/src/main/java/com/squirtles/core/picklist/PickListType.kt similarity index 51% rename from app/src/main/java/com/squirtles/musicroad/common/picklist/PickListType.kt rename to core/picklist/src/main/java/com/squirtles/core/picklist/PickListType.kt index e357f48e..dbdfe65c 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/picklist/PickListType.kt +++ b/core/picklist/src/main/java/com/squirtles/core/picklist/PickListType.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common.picklist +package com.squirtles.core.picklist enum class PickListType { FAVORITE, CREATED diff --git a/app/src/main/java/com/squirtles/musicroad/common/picklist/PickListUiState.kt b/core/picklist/src/main/java/com/squirtles/core/picklist/PickListUiState.kt similarity index 62% rename from app/src/main/java/com/squirtles/musicroad/common/picklist/PickListUiState.kt rename to core/picklist/src/main/java/com/squirtles/core/picklist/PickListUiState.kt index f0df5a8c..4237c81c 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/picklist/PickListUiState.kt +++ b/core/picklist/src/main/java/com/squirtles/core/picklist/PickListUiState.kt @@ -1,7 +1,7 @@ -package com.squirtles.musicroad.common.picklist +package com.squirtles.core.picklist -import com.squirtles.domain.model.Order -import com.squirtles.domain.model.Pick +import com.squirtles.core.model.Order +import com.squirtles.core.model.Pick sealed class PickListUiState { data object Loading : PickListUiState() diff --git a/app/src/main/java/com/squirtles/musicroad/common/picklist/PickListViewModel.kt b/core/picklist/src/main/java/com/squirtles/core/picklist/PickListViewModel.kt similarity index 76% rename from app/src/main/java/com/squirtles/musicroad/common/picklist/PickListViewModel.kt rename to core/picklist/src/main/java/com/squirtles/core/picklist/PickListViewModel.kt index 4221d568..a34c910c 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/picklist/PickListViewModel.kt +++ b/core/picklist/src/main/java/com/squirtles/core/picklist/PickListViewModel.kt @@ -1,27 +1,28 @@ -package com.squirtles.musicroad.common.picklist +package com.squirtles.core.picklist import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.squirtles.domain.model.Order -import com.squirtles.domain.model.Pick -import com.squirtles.domain.usecase.picklist.DeletePickListUseCaseInterface -import com.squirtles.domain.usecase.picklist.FetchPickListUseCaseInterface -import com.squirtles.domain.usecase.picklist.GetPickListOrderUseCaseInterface -import com.squirtles.domain.usecase.picklist.SavePickListOrderUseCaseInterface -import com.squirtles.domain.usecase.user.GetCurrentUidUseCase +import com.squirtles.domain.picklist.FetchPickListUseCaseInterface +import com.squirtles.domain.picklist.GetPickListOrderUseCaseInterface +import com.squirtles.domain.picklist.RemovePickUseCaseInterface +import com.squirtles.domain.picklist.SavePickListOrderUseCaseInterface +import com.squirtles.core.model.Order +import com.squirtles.core.model.Pick +import com.squirtles.domain.user.usecase.GetCurrentUidUseCase import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch - -abstract class PickListViewModel( - val fetchPickListUseCase: FetchPickListUseCaseInterface, - val getPickListOrderUseCase: GetPickListOrderUseCaseInterface, - val savePickListOrderUseCase: SavePickListOrderUseCaseInterface, - val removePickUseCase: DeletePickListUseCaseInterface, - val getCurrentUidUseCase: GetCurrentUidUseCase +import javax.inject.Inject + +open class PickListViewModel @Inject constructor( + private val fetchPickListUseCase: FetchPickListUseCaseInterface, + private val getPickListOrderUseCase: GetPickListOrderUseCaseInterface, + private val savePickListOrderUseCase: SavePickListOrderUseCaseInterface, + private val removePickUseCase: RemovePickUseCaseInterface, + private val getCurrentUidUseCase: GetCurrentUidUseCase ) : ViewModel() { private var pickList: List = emptyList() diff --git a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/DeleteSelectedPickDialog.kt b/core/picklist/src/main/java/com/squirtles/core/picklist/components/DeleteSelectedPickDialog.kt similarity index 77% rename from app/src/main/java/com/squirtles/musicroad/common/picklist/components/DeleteSelectedPickDialog.kt rename to core/picklist/src/main/java/com/squirtles/core/picklist/components/DeleteSelectedPickDialog.kt index bd83d6b7..7944c572 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/DeleteSelectedPickDialog.kt +++ b/core/picklist/src/main/java/com/squirtles/core/picklist/components/DeleteSelectedPickDialog.kt @@ -1,14 +1,14 @@ -package com.squirtles.musicroad.common.picklist.components +package com.squirtles.core.picklist.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.DialogTextButton -import com.squirtles.musicroad.common.HorizontalSpacer -import com.squirtles.musicroad.common.MessageAlertDialog -import com.squirtles.musicroad.common.picklist.PickListType -import com.squirtles.musicroad.ui.theme.Primary +import com.squirtles.core.common.ui.DialogTextButton +import com.squirtles.core.common.ui.HorizontalSpacer +import com.squirtles.core.common.ui.MessageAlertDialog +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.picklist.PickListType +import com.squirtles.core.picklist.R @Composable internal fun DeleteSelectedPickDialog( diff --git a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/EditModeAction.kt b/core/picklist/src/main/java/com/squirtles/core/picklist/components/EditModeAction.kt similarity index 90% rename from app/src/main/java/com/squirtles/musicroad/common/picklist/components/EditModeAction.kt rename to core/picklist/src/main/java/com/squirtles/core/picklist/components/EditModeAction.kt index e904a8dd..f9a8cb42 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/EditModeAction.kt +++ b/core/picklist/src/main/java/com/squirtles/core/picklist/components/EditModeAction.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common.picklist.components +package com.squirtles.core.picklist.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit @@ -9,9 +9,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.picklist.R + @Composable internal fun EditModeAction( diff --git a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/EditModeBottomButton.kt b/core/picklist/src/main/java/com/squirtles/core/picklist/components/EditModeBottomButton.kt similarity index 88% rename from app/src/main/java/com/squirtles/musicroad/common/picklist/components/EditModeBottomButton.kt rename to core/picklist/src/main/java/com/squirtles/core/picklist/components/EditModeBottomButton.kt index a8014244..11c50c5b 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/EditModeBottomButton.kt +++ b/core/picklist/src/main/java/com/squirtles/core/picklist/components/EditModeBottomButton.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common.picklist.components +package com.squirtles.core.picklist.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight @@ -15,12 +15,12 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.DarkGray -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.MusicRoadTheme -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.theme.DarkGray +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.picklist.R @Composable internal fun EditModeBottomButton( diff --git a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/OrderBottomSheet.kt b/core/picklist/src/main/java/com/squirtles/core/picklist/components/OrderBottomSheet.kt similarity index 93% rename from app/src/main/java/com/squirtles/musicroad/common/picklist/components/OrderBottomSheet.kt rename to core/picklist/src/main/java/com/squirtles/core/picklist/components/OrderBottomSheet.kt index 401c0997..21bc37b1 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/OrderBottomSheet.kt +++ b/core/picklist/src/main/java/com/squirtles/core/picklist/components/OrderBottomSheet.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common.picklist.components +package com.squirtles.core.picklist.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -25,12 +25,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import com.squirtles.domain.model.Order -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.Constants.DEFAULT_PADDING -import com.squirtles.musicroad.ui.theme.Dark -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.Constants.DEFAULT_PADDING +import com.squirtles.core.common.ui.theme.Dark +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.model.Order +import com.squirtles.core.picklist.R import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/PickItem.kt b/core/picklist/src/main/java/com/squirtles/core/picklist/components/PickItem.kt similarity index 86% rename from app/src/main/java/com/squirtles/musicroad/common/picklist/components/PickItem.kt rename to core/picklist/src/main/java/com/squirtles/core/picklist/components/PickItem.kt index 54812bad..d155cff3 100644 --- a/app/src/main/java/com/squirtles/musicroad/common/picklist/components/PickItem.kt +++ b/core/picklist/src/main/java/com/squirtles/core/picklist/components/PickItem.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.common.picklist.components +package com.squirtles.core.picklist.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -25,18 +25,18 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.AlbumImage -import com.squirtles.musicroad.common.CommentText -import com.squirtles.musicroad.common.Constants -import com.squirtles.musicroad.common.CreatedByOtherUserText -import com.squirtles.musicroad.common.FavoriteCountText -import com.squirtles.musicroad.common.HorizontalSpacer -import com.squirtles.musicroad.common.SongInfoText -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.AlbumImage +import com.squirtles.core.common.ui.CommentText +import com.squirtles.core.common.ui.Constants +import com.squirtles.core.common.ui.CreatedByOtherUserText +import com.squirtles.core.common.ui.FavoriteCountText +import com.squirtles.core.common.ui.HorizontalSpacer +import com.squirtles.core.common.ui.SongInfoText +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.model.Song +import com.squirtles.core.picklist.R @Composable internal fun PickItem( diff --git a/core/picklist/src/main/res/values/strings.xml b/core/picklist/src/main/res/values/strings.xml new file mode 100644 index 00000000..81080ca5 --- /dev/null +++ b/core/picklist/src/main/res/values/strings.xml @@ -0,0 +1,36 @@ + + 최근 등록순 + 최근 담은순 + 과거 담은순 + 과거 등록순 + 담기 많은순 + 체크 아이콘 + 닫기 + + 픽 보관함 + 등록한 픽 + + 담은 픽이 없습니다 + 등록한 픽이 없습니다 + 일시적인 오류가 발생했습니다. + + + 삭제하시겠습니까? + 등록하신 픽이 삭제됩니다. + 취소 + 삭제하기 + 픽 보관함에서 %d개의 픽이 삭제됩니다. + 선택하신 %d개의 픽이 삭제됩니다. + + + 취소 + 선택 삭제 + 원 윤곽선이 있는 체크 아이콘 + 전체 + 선택 + + + 픽 목록 편집 모드 활성화 버튼 + 전체 선택 + 선택 해제 + diff --git a/core/picklist/src/test/java/com/squirtles/core/picklist/ExampleUnitTest.kt b/core/picklist/src/test/java/com/squirtles/core/picklist/ExampleUnitTest.kt new file mode 100644 index 00000000..be0b9f6c --- /dev/null +++ b/core/picklist/src/test/java/com/squirtles/core/picklist/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.squirtles.picklist + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/core/util/.gitignore b/core/util/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/util/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/util/build.gradle.kts b/core/util/build.gradle.kts new file mode 100644 index 00000000..000e3030 --- /dev/null +++ b/core/util/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(libs.plugins.musicroad.android.library) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.squirtles.core.util" +} + +dependencies { + implementation(libs.androidx.navigation.common.ktx) + + // Serialization + implementation(libs.kotlinx.serialization.json) +} diff --git a/core/util/proguard-rules.pro b/core/util/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/util/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/java/com/squirtles/musicroad/utils/SerializableType.kt b/core/util/src/main/java/com/squirtles/core/util/SerializableType.kt similarity index 95% rename from app/src/main/java/com/squirtles/musicroad/utils/SerializableType.kt rename to core/util/src/main/java/com/squirtles/core/util/SerializableType.kt index 3741f31c..a3d376b5 100644 --- a/app/src/main/java/com/squirtles/musicroad/utils/SerializableType.kt +++ b/core/util/src/main/java/com/squirtles/core/util/SerializableType.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.utils +package com.squirtles.core.util import android.os.Bundle import androidx.navigation.NavType diff --git a/app/src/main/java/com/squirtles/musicroad/utils/ThrottleFirst.kt b/core/util/src/main/java/com/squirtles/core/util/ThrottleFirst.kt similarity index 79% rename from app/src/main/java/com/squirtles/musicroad/utils/ThrottleFirst.kt rename to core/util/src/main/java/com/squirtles/core/util/ThrottleFirst.kt index cc768826..269ed3bb 100644 --- a/app/src/main/java/com/squirtles/musicroad/utils/ThrottleFirst.kt +++ b/core/util/src/main/java/com/squirtles/core/util/ThrottleFirst.kt @@ -1,9 +1,9 @@ -package com.squirtles.musicroad.utils +package com.squirtles.core.util import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -internal fun Flow.throttleFirst(periodMillis: Long): Flow { +fun Flow.throttleFirst(periodMillis: Long): Flow { require(periodMillis > 0) { "period should be positive" } return flow { var lastTime = 0L diff --git a/data/applemusic/.gitignore b/data/applemusic/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/applemusic/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/applemusic/build.gradle.kts b/data/applemusic/build.gradle.kts new file mode 100644 index 00000000..0a723a27 --- /dev/null +++ b/data/applemusic/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id(libs.plugins.musicroad.data.get().pluginId) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.squirtles.data.applemusic" +} + +dependencies { + implementation(projects.domain.applemusic) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + implementation(libs.androidx.paging.runtime) + + // Kotlinx Serialization + implementation(libs.kotlinx.serialization.json) + + // OkHttp + implementation(libs.bundles.network) +} diff --git a/data/applemusic/proguard-rules.pro b/data/applemusic/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/data/applemusic/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/applemusic/src/main/java/com/squirtles/data/applemusic/AppleMusicDataSource.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/AppleMusicDataSource.kt new file mode 100644 index 00000000..375727f8 --- /dev/null +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/AppleMusicDataSource.kt @@ -0,0 +1,11 @@ +package com.squirtles.data.applemusic + +import androidx.paging.PagingData +import com.squirtles.core.model.MusicVideo +import com.squirtles.core.model.Song +import kotlinx.coroutines.flow.Flow + +interface AppleMusicDataSource { + fun searchSongs(searchText: String): Flow> + suspend fun searchMusicVideos(searchText: String): List +} diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/AppleMusicDataSourceImpl.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/AppleMusicDataSourceImpl.kt similarity index 71% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/AppleMusicDataSourceImpl.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/AppleMusicDataSourceImpl.kt index 38d8f8f4..f388b8a8 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/AppleMusicDataSourceImpl.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/AppleMusicDataSourceImpl.kt @@ -1,22 +1,21 @@ -package com.squirtles.data.datasource.remote.applemusic +package com.squirtles.data.applemusic import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData -import com.squirtles.data.datasource.remote.applemusic.SearchSongsPagingSource.Companion.SEARCH_PAGE_SIZE -import com.squirtles.data.datasource.remote.applemusic.api.AppleMusicApi -import com.squirtles.data.datasource.remote.applemusic.model.SearchResponse -import com.squirtles.data.mapper.toMusicVideo -import com.squirtles.domain.applemusic.AppleMusicRemoteDataSource -import com.squirtles.domain.model.MusicVideo -import com.squirtles.domain.model.Song +import com.squirtles.data.applemusic.SearchSongsPagingSource.Companion.SEARCH_PAGE_SIZE +import com.squirtles.data.applemusic.api.AppleMusicApi +import com.squirtles.data.applemusic.model.SearchResponse +import com.squirtles.data.applemusic.model.toMusicVideo +import com.squirtles.core.model.MusicVideo +import com.squirtles.core.model.Song import kotlinx.coroutines.flow.Flow import retrofit2.Response import javax.inject.Inject class AppleMusicDataSourceImpl @Inject constructor( private val appleMusicApi: AppleMusicApi -) : AppleMusicRemoteDataSource { +) : AppleMusicDataSource { override fun searchSongs(searchText: String): Flow> { return Pager( @@ -28,10 +27,6 @@ class AppleMusicDataSourceImpl @Inject constructor( ).flow } - override suspend fun searchSongById(songId: String): Song { - TODO("Not yet implemented") - } - override suspend fun searchMusicVideos(searchText: String): List { val searchResult = requestSearchApi(searchText, "music-videos") return searchResult.results.musicVideos?.data?.map { diff --git a/data/src/main/java/com/squirtles/data/repository/AppleMusicRepositoryImpl.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/AppleMusicRepositoryImpl.kt similarity index 59% rename from data/src/main/java/com/squirtles/data/repository/AppleMusicRepositoryImpl.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/AppleMusicRepositoryImpl.kt index 1b8c20a7..2fe6ba1b 100644 --- a/data/src/main/java/com/squirtles/data/repository/AppleMusicRepositoryImpl.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/AppleMusicRepositoryImpl.kt @@ -1,23 +1,19 @@ -package com.squirtles.data.repository +package com.squirtles.data.applemusic import androidx.paging.PagingData -import com.squirtles.domain.applemusic.AppleMusicRemoteDataSource import com.squirtles.domain.applemusic.AppleMusicException -import com.squirtles.domain.model.MusicVideo -import com.squirtles.domain.model.Song import com.squirtles.domain.applemusic.AppleMusicRepository +import com.squirtles.core.model.MusicVideo +import com.squirtles.core.model.Song import kotlinx.coroutines.flow.Flow import javax.inject.Inject class AppleMusicRepositoryImpl @Inject constructor( - private val appleMusicDataSource: AppleMusicRemoteDataSource + private val appleMusicDataSource: AppleMusicDataSource ) : AppleMusicRepository { - override fun searchSongs(searchText: String): Flow> = appleMusicDataSource.searchSongs(searchText) - - override suspend fun searchSongById(songId: String): Result { - TODO("Not yet implemented") - } + override fun searchSongs(searchText: String): Flow> = + appleMusicDataSource.searchSongs(searchText) override suspend fun searchMusicVideos(searchText: String): Result> { return handleResult(AppleMusicException.NotFoundException()) { @@ -33,12 +29,4 @@ class AppleMusicRepositoryImpl @Inject constructor( call() ?: throw appleMusicException } } - - private suspend fun handleResult( - call: suspend () -> T - ): Result { - return runCatching { - call() - } - } } diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/SearchSongsPagingSource.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/SearchSongsPagingSource.kt similarity index 90% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/SearchSongsPagingSource.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/SearchSongsPagingSource.kt index 1e2bf29c..ade133a1 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/SearchSongsPagingSource.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/SearchSongsPagingSource.kt @@ -1,10 +1,10 @@ -package com.squirtles.data.datasource.remote.applemusic +package com.squirtles.data.applemusic import androidx.paging.PagingSource import androidx.paging.PagingState -import com.squirtles.data.datasource.remote.applemusic.api.AppleMusicApi -import com.squirtles.data.mapper.toSong -import com.squirtles.domain.model.Song +import com.squirtles.data.applemusic.api.AppleMusicApi +import com.squirtles.data.applemusic.model.toSong +import com.squirtles.core.model.Song import retrofit2.HttpException import java.io.IOException @@ -58,4 +58,4 @@ class SearchSongsPagingSource( const val SEARCH_TYPES = "songs" const val SEARCH_PAGE_SIZE = 10 } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/api/AppleMusicApi.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/api/AppleMusicApi.kt similarity index 72% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/api/AppleMusicApi.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/api/AppleMusicApi.kt index 2451700f..03eb3dff 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/api/AppleMusicApi.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/api/AppleMusicApi.kt @@ -1,11 +1,10 @@ -package com.squirtles.data.datasource.remote.applemusic.api +package com.squirtles.data.applemusic.api -import com.squirtles.data.datasource.remote.applemusic.model.SearchResponse +import com.squirtles.data.applemusic.model.SearchResponse import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query -import retrofit2.http.QueryMap interface AppleMusicApi { @GET("v1/catalog/{storefront}/search") diff --git a/data/src/main/java/com/squirtles/data/di/ApiModule.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/api/NetworkModule.kt similarity index 61% rename from data/src/main/java/com/squirtles/data/di/ApiModule.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/api/NetworkModule.kt index 75e98ec8..178dfa50 100644 --- a/data/src/main/java/com/squirtles/data/di/ApiModule.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/api/NetworkModule.kt @@ -1,7 +1,6 @@ -package com.squirtles.data.di +package com.squirtles.data.applemusic.api -import com.squirtles.data.BuildConfig -import com.squirtles.data.datasource.remote.applemusic.api.AppleMusicApi +import com.squirtles.core.buildconfig.LocalPropertyProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,16 +10,12 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Converter -import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object ApiModule { - - private const val BASE_URL = "" - private const val BASE_APPLE_MUSIC_URL = "https://api.music.apple.com/" +object NetworkModule { @Provides @Singleton @@ -30,7 +25,7 @@ internal object ApiModule { return OkHttpClient.Builder() .addInterceptor { chain -> val newRequest = chain.request().newBuilder() - .addHeader("Authorization", "Bearer ${BuildConfig.APPLE_MUSIC_API_TOKEN}") + .addHeader("Authorization", "Bearer ${LocalPropertyProvider.appleMusicApiToken}") .build() chain.proceed(newRequest) } @@ -46,20 +41,6 @@ internal object ApiModule { return json.asConverterFactory("application/json".toMediaType()) } - @Provides - @Singleton - fun provideAppleMusicApi( - appleOkHttpClient: OkHttpClient, - converterFactory: Converter.Factory, - ): AppleMusicApi { - return Retrofit.Builder() - .baseUrl(BASE_APPLE_MUSIC_URL) - .addConverterFactory(converterFactory) - .client(appleOkHttpClient) - .build() - .create(AppleMusicApi::class.java) - } - @Provides @Singleton fun provideJson(): Json = Json { diff --git a/data/applemusic/src/main/java/com/squirtles/data/applemusic/di/AppleMusicDiModule.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/di/AppleMusicDiModule.kt new file mode 100644 index 00000000..cfb8f0ab --- /dev/null +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/di/AppleMusicDiModule.kt @@ -0,0 +1,46 @@ +package com.squirtles.data.applemusic.di + +import com.squirtles.data.applemusic.AppleMusicDataSource +import com.squirtles.data.applemusic.AppleMusicDataSourceImpl +import com.squirtles.domain.applemusic.AppleMusicRepository +import com.squirtles.data.applemusic.AppleMusicRepositoryImpl +import com.squirtles.data.applemusic.api.AppleMusicApi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import retrofit2.Converter +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppleMusicDiModule{ + + private const val BASE_APPLE_MUSIC_URL = "https://api.music.apple.com/" + + @Provides + @Singleton + fun provideAppleMusicApi( + appleOkHttpClient: OkHttpClient, + converterFactory: Converter.Factory, + ): AppleMusicApi { + return Retrofit.Builder() + .baseUrl(BASE_APPLE_MUSIC_URL) + .addConverterFactory(converterFactory) + .client(appleOkHttpClient) + .build() + .create(AppleMusicApi::class.java) + } + + @Provides + @Singleton + fun provideAppleMusicRepository(appleMusicDataSource: AppleMusicDataSource): AppleMusicRepository = + AppleMusicRepositoryImpl(appleMusicDataSource) + + @Provides + @Singleton + fun provideAppleMusicDataSource(api: AppleMusicApi): AppleMusicDataSource = + AppleMusicDataSourceImpl(api) +} diff --git a/data/src/main/java/com/squirtles/data/mapper/AppleMusicMapper.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/AppleMusicMapper.kt similarity index 80% rename from data/src/main/java/com/squirtles/data/mapper/AppleMusicMapper.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/model/AppleMusicMapper.kt index 12ecd493..ba111fe7 100644 --- a/data/src/main/java/com/squirtles/data/mapper/AppleMusicMapper.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/AppleMusicMapper.kt @@ -1,13 +1,12 @@ -package com.squirtles.data.mapper +package com.squirtles.data.applemusic.model import androidx.core.graphics.toColorInt -import com.squirtles.data.datasource.remote.applemusic.model.Data -import com.squirtles.domain.model.MusicVideo -import com.squirtles.domain.model.Song +import com.squirtles.core.model.MusicVideo +import com.squirtles.core.model.Song import java.time.LocalDate import java.time.format.DateTimeFormatter -fun Data.toSong(): Song = Song( +internal fun Data.toSong(): Song = Song( id = id, songName = this.attributes.songName, artistName = this.attributes.artistName, @@ -19,7 +18,7 @@ fun Data.toSong(): Song = Song( previewUrl = this.attributes.previews[0].url.toString(), ) -fun Data.toMusicVideo(): MusicVideo = MusicVideo( +internal fun Data.toMusicVideo(): MusicVideo = MusicVideo( id = id, songName = this.attributes.songName, artistName = this.attributes.artistName, diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Artwork.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Artwork.kt similarity index 73% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Artwork.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Artwork.kt index f62d864a..ccdcc5e2 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Artwork.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Artwork.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.applemusic.model +package com.squirtles.data.applemusic.model import kotlinx.serialization.Serializable diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Attributes.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Attributes.kt similarity index 90% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Attributes.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Attributes.kt index 6579a133..ecd71865 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Attributes.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Attributes.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.applemusic.model +package com.squirtles.data.applemusic.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Data.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Data.kt similarity index 67% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Data.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Data.kt index a2a491a2..5d8dcf82 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Data.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Data.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.applemusic.model +package com.squirtles.data.applemusic.model import kotlinx.serialization.Serializable diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/MusicVideoResponse.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/MusicVideoResponse.kt similarity index 65% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/MusicVideoResponse.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/model/MusicVideoResponse.kt index d0626150..1dc83d54 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/MusicVideoResponse.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/MusicVideoResponse.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.applemusic.model +package com.squirtles.data.applemusic.model import kotlinx.serialization.Serializable diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Preview.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Preview.kt similarity index 73% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Preview.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Preview.kt index 119e75db..2253cbc9 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Preview.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Preview.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.applemusic.model +package com.squirtles.data.applemusic.model import kotlinx.serialization.Serializable diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Results.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Results.kt similarity index 79% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Results.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Results.kt index 4cf960f2..5183ae97 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Results.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Results.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.applemusic.model +package com.squirtles.data.applemusic.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/SearchResponse.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/SearchResponse.kt similarity index 64% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/SearchResponse.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/model/SearchResponse.kt index 13e292d5..e184087e 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/SearchResponse.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/SearchResponse.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.applemusic.model +package com.squirtles.data.applemusic.model import kotlinx.serialization.Serializable diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Songs.kt b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Songs.kt similarity index 68% rename from data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Songs.kt rename to data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Songs.kt index d49c03eb..f4fba366 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/applemusic/model/Songs.kt +++ b/data/applemusic/src/main/java/com/squirtles/data/applemusic/model/Songs.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.applemusic.model +package com.squirtles.data.applemusic.model import kotlinx.serialization.Serializable diff --git a/data/build.gradle.kts b/data/build.gradle.kts deleted file mode 100644 index e8c916e5..00000000 --- a/data/build.gradle.kts +++ /dev/null @@ -1,119 +0,0 @@ -import java.io.FileInputStream -import java.util.Properties - -var properties = Properties() -properties.load(FileInputStream("local.properties")) - -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.jetbrains.kotlin.android) - alias(libs.plugins.ksp) - alias(libs.plugins.hilt) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "com.squirtles.data" - compileSdk = 34 - - defaultConfig { - minSdk = 26 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - - buildConfigField( - "String", - "APPLE_MUSIC_API_TOKEN", - "\"${properties.getProperty("APPLE_MUSIC_API_TOKEN")}\"" - ) - - } - - buildTypes { - debug { - isMinifyEnabled = false - - buildConfigField( - "String", - "FIRESTORE_DB_ID", - "\"${properties.getProperty("FIRESTORE_DB_ID_DEBUG")}\"" - ) - - buildConfigField( - "String", - "HTTPS_CALLABLE", - "\"${properties.getProperty("HTTPS_CALLABLE_DEBUG")}\"" - ) - } - - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - - buildConfigField( - "String", - "FIRESTORE_DB_ID", - "\"${properties.getProperty("FIRESTORE_DB_ID_RELEASE")}\"" - ) - - buildConfigField( - "String", - "HTTPS_CALLABLE", - "\"${properties.getProperty("HTTPS_CALLABLE_RELEASE")}\"" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" - } - buildFeatures { - buildConfig = true - } -} - -dependencies { - implementation(projects.domain) - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - - // Firebase - implementation(libs.firebase.firestore.ktx) - implementation(libs.firebase.functions.ktx) - implementation(libs.geofire.android.common) - implementation(libs.kotlinx.coroutines.play.services) - - // Hilt - implementation(libs.hilt.android) - ksp(libs.hilt.android.compiler) - androidTestImplementation(libs.hilt.android.testing) - kspAndroidTest(libs.hilt.android.compiler) - - // OkHttp - implementation(libs.okhttp.logging) - - // Retrofit - implementation(libs.retrofit.core) - implementation(libs.retrofit.kotlin.serialization) - - // Kotlinx Serialization - implementation(libs.kotlinx.serialization.json) - - // Datastore - implementation(libs.androidx.datastore.preferences) - - // Paging - implementation(libs.androidx.paging.runtime) -} diff --git a/data/favorite/.gitignore b/data/favorite/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/favorite/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/favorite/build.gradle.kts b/data/favorite/build.gradle.kts new file mode 100644 index 00000000..10a501b5 --- /dev/null +++ b/data/favorite/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id(libs.plugins.musicroad.data.get().pluginId) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.squirtles.data.favorite" +} + +dependencies { + implementation(projects.data.firebase) + implementation(projects.domain.firebase) + implementation(projects.domain.favorite) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + // Kotlinx Serialization + implementation(libs.kotlinx.serialization.json) + + // Firebase + implementation(libs.bundles.firebase) +} diff --git a/data/favorite/proguard-rules.pro b/data/favorite/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/data/favorite/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/CloudFunctionHelper.kt b/data/favorite/src/main/java/com/squirtles/data/favorite/CloudFunctionHelper.kt similarity index 57% rename from data/src/main/java/com/squirtles/data/datasource/remote/firebase/CloudFunctionHelper.kt rename to data/favorite/src/main/java/com/squirtles/data/favorite/CloudFunctionHelper.kt index 96b43a41..78e4cba5 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/CloudFunctionHelper.kt +++ b/data/favorite/src/main/java/com/squirtles/data/favorite/CloudFunctionHelper.kt @@ -1,19 +1,23 @@ -package com.squirtles.data.datasource.remote.firebase +package com.squirtles.data.favorite +import android.util.Log import com.google.firebase.functions.FirebaseFunctions import com.google.firebase.functions.ktx.functions import com.google.firebase.ktx.Firebase -import com.squirtles.data.BuildConfig +import com.squirtles.domain.firebase.FirebaseException +import com.squirtles.core.buildconfig.LocalPropertyProvider import kotlinx.coroutines.tasks.await +import javax.inject.Singleton +@Singleton class CloudFunctionHelper { private val functions: FirebaseFunctions = Firebase.functions suspend fun updateFavoriteCount(pickId: String): Result { - return try { + return runCatching { val data = hashMapOf("pickId" to pickId) val result = functions - .getHttpsCallable(BuildConfig.HTTPS_CALLABLE) + .getHttpsCallable(LocalPropertyProvider.httpsCallable) .call(data) .await() @@ -21,10 +25,10 @@ class CloudFunctionHelper { val message = result.getData()?.let { (it as? Map<*, *>)?.get("message") as? String ?: "Function executed successfully" } ?: "No message in response" - Result.success(message) - } catch (e: Exception) { - // 에러 처리 - Result.failure(e) + message + }.onFailure { + Log.d("CloudFunctionHelper", "Error updating favorite count: ${it.message}") + throw FirebaseException.CloudFunctionFailedException(exceptionMessage = it.message.toString()) } } } diff --git a/data/favorite/src/main/java/com/squirtles/data/favorite/FirebaseFavoriteDataSource.kt b/data/favorite/src/main/java/com/squirtles/data/favorite/FirebaseFavoriteDataSource.kt new file mode 100644 index 00000000..5af7dd81 --- /dev/null +++ b/data/favorite/src/main/java/com/squirtles/data/favorite/FirebaseFavoriteDataSource.kt @@ -0,0 +1,7 @@ +package com.squirtles.data.favorite + +interface FirebaseFavoriteDataSource { + suspend fun fetchIsFavorite(pickId: String, userId: String): Result + suspend fun createFavorite(pickId: String, userId: String): Result + suspend fun deleteFavorite(pickId: String, userId: String): Result +} diff --git a/data/favorite/src/main/java/com/squirtles/data/favorite/FirebaseFavoriteDataSourceImpl.kt b/data/favorite/src/main/java/com/squirtles/data/favorite/FirebaseFavoriteDataSourceImpl.kt new file mode 100644 index 00000000..7a629742 --- /dev/null +++ b/data/favorite/src/main/java/com/squirtles/data/favorite/FirebaseFavoriteDataSourceImpl.kt @@ -0,0 +1,70 @@ +package com.squirtles.data.favorite + +import android.util.Log +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.QuerySnapshot +import com.squirtles.data.firebase.BaseFirebaseDataSource +import com.squirtles.data.firebase.FirebaseCollections +import com.squirtles.data.firebase.FirebaseDocumentFields +import com.squirtles.data.firebase.model.FirebaseFavorite +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FirebaseFavoriteDataSourceImpl @Inject constructor( + db: FirebaseFirestore, + private val cloudFunctionHelper: CloudFunctionHelper +) : BaseFirebaseDataSource(db), FirebaseFavoriteDataSource { + + override suspend fun fetchIsFavorite(pickId: String, userId: String): Result { + return runCatching { + val favoriteDocument = queryFavoriteByPickIdAndUserId(pickId, userId).getOrThrow() + favoriteDocument.isEmpty.not() + } + } + + override suspend fun createFavorite(pickId: String, userId: String): Result { + val firebaseFavorite = FirebaseFavorite( + pickId = pickId, + uid = userId + ) + return runCatching { + val addResult = addDocument(FirebaseCollections.Favorites, firebaseFavorite) + val functionResultMessage = updateFavoriteCount(pickId).getOrThrow() + Log.d(TAG_LOG, "Function result message: $functionResultMessage") // XXX + + addResult.getOrThrow().id + } + } + + override suspend fun deleteFavorite(pickId: String, userId: String): Result { + return runCatching { + val favoriteDocRef = queryFavoriteByPickIdAndUserId(pickId, userId).getOrThrow().documents.first().reference + + deleteDocument(favoriteDocRef) + + val functionResultMessage = updateFavoriteCount(pickId).getOrThrow() + + Log.d(TAG_LOG, "Function result message: $functionResultMessage") // XXX + + favoriteDocRef.id + }.onFailure { exception -> + Log.w(TAG_LOG, "Error deleting favorite document", exception) + } + } + + private suspend fun queryFavoriteByPickIdAndUserId(pickId: String, userId: String): Result = + queryDocumentsEquals( + collection = FirebaseCollections.Favorites, + fields = listOf(FirebaseDocumentFields.PickId, FirebaseDocumentFields.Uid), + values = listOf(pickId, userId) + ) + + private suspend fun updateFavoriteCount(pickId: String): Result { + return cloudFunctionHelper.updateFavoriteCount(pickId) + } + + companion object { + private const val TAG_LOG = "FirebaseFavoriteDataSourceImpl" + } +} diff --git a/data/favorite/src/main/java/com/squirtles/data/favorite/FirebaseFavoriteRepositoryImpl.kt b/data/favorite/src/main/java/com/squirtles/data/favorite/FirebaseFavoriteRepositoryImpl.kt new file mode 100644 index 00000000..8632ec5a --- /dev/null +++ b/data/favorite/src/main/java/com/squirtles/data/favorite/FirebaseFavoriteRepositoryImpl.kt @@ -0,0 +1,23 @@ +package com.squirtles.data.favorite + +import com.squirtles.domain.favorite.FirebaseFavoriteRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FirebaseFavoriteRepositoryImpl @Inject constructor( + private val favoriteDataSource: FirebaseFavoriteDataSource +) : FirebaseFavoriteRepository { + + override suspend fun fetchIsFavorite(pickId: String, uid: String): Result { + return favoriteDataSource.fetchIsFavorite(pickId, uid) + } + + override suspend fun createFavorite(pickId: String, uid: String): Result { + return favoriteDataSource.createFavorite(pickId, uid) + } + + override suspend fun deleteFavorite(pickId: String, uid: String): Result { + return favoriteDataSource.deleteFavorite(pickId, uid) + } +} diff --git a/data/favorite/src/main/java/com/squirtles/data/favorite/di/FavoriteDiModule.kt b/data/favorite/src/main/java/com/squirtles/data/favorite/di/FavoriteDiModule.kt new file mode 100644 index 00000000..733544cb --- /dev/null +++ b/data/favorite/src/main/java/com/squirtles/data/favorite/di/FavoriteDiModule.kt @@ -0,0 +1,35 @@ +package com.squirtles.data.favorite.di + +import com.squirtles.data.favorite.CloudFunctionHelper +import com.squirtles.data.favorite.FirebaseFavoriteDataSource +import com.squirtles.data.favorite.FirebaseFavoriteDataSourceImpl +import com.squirtles.domain.favorite.FirebaseFavoriteRepository +import com.squirtles.data.favorite.FirebaseFavoriteRepositoryImpl +import com.google.firebase.firestore.FirebaseFirestore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object FavoriteDiModule { + + @Provides + @Singleton + fun provideFirebaseFavoriteRepository(firebaseFavoriteDataSource: FirebaseFavoriteDataSource): FirebaseFavoriteRepository = + FirebaseFavoriteRepositoryImpl(firebaseFavoriteDataSource) + + @Provides + @Singleton + fun provideFirebaseFavoriteDataSource( + db: FirebaseFirestore, + cloudFunctionHelper: CloudFunctionHelper + ): FirebaseFavoriteDataSource = + FirebaseFavoriteDataSourceImpl(db, cloudFunctionHelper) + + @Provides + @Singleton + fun provideCloudFunctionHelper(): CloudFunctionHelper = CloudFunctionHelper() +} diff --git a/data/firebase/.gitignore b/data/firebase/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/firebase/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/firebase/build.gradle.kts b/data/firebase/build.gradle.kts new file mode 100644 index 00000000..3b61ea8f --- /dev/null +++ b/data/firebase/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id(libs.plugins.musicroad.data.get().pluginId) +} + +android { + namespace = "com.squirtles.data.firebase" +} + +dependencies { + implementation(projects.domain.firebase) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + // Firebase + implementation(libs.firebase.firestore.ktx) + implementation(libs.geofire.android.common) +} diff --git a/data/firebase/proguard-rules.pro b/data/firebase/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/data/firebase/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/firebase/src/main/java/com/squirtles/data/firebase/BaseFirebaseDataSource.kt b/data/firebase/src/main/java/com/squirtles/data/firebase/BaseFirebaseDataSource.kt new file mode 100644 index 00000000..ee2bfb44 --- /dev/null +++ b/data/firebase/src/main/java/com/squirtles/data/firebase/BaseFirebaseDataSource.kt @@ -0,0 +1,137 @@ +package com.squirtles.data.firebase + +import android.util.Log +import com.google.firebase.firestore.CollectionReference +import com.google.firebase.firestore.DocumentReference +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.EventListener +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ListenerRegistration +import com.google.firebase.firestore.Query +import com.google.firebase.firestore.QuerySnapshot +import com.squirtles.domain.firebase.FirebaseException +import kotlinx.coroutines.tasks.await + +open class BaseFirebaseDataSource( + private val db: FirebaseFirestore +) { + protected fun fetchCollection(collection: FirebaseCollections): CollectionReference = db.collection(collection.name) + + protected fun fetchDocumentReference(collection: FirebaseCollections, documentId: String): DocumentReference = + fetchCollection(collection).document(documentId) + + protected suspend fun fetchDocumentSnapshot(collection: FirebaseCollections, documentId: String): Result { + return runCatching { + fetchCollection(collection).document(documentId).get().await().ifNotExist { + throw FirebaseException.NoSuchDocumentException(docId = documentId, collection = collection.name) + } + }.onFailure { + Log.e("FirebaseDataSource", "Failed to fetch document snapshot", it) + throw FirebaseException.FetchDocumentFailedException(collection = collection.name) + } + } + + protected suspend fun queryDocumentsEquals( + collection: FirebaseCollections, + fields: List, + values: List + ): Result { + return runCatching { + var query: Query = fetchCollection(collection) + fields.forEachIndexed { index, field -> + query = query.whereEqualTo(field.name, values[index]) + } + query.get().await() + }.onFailure { + Log.e("FirebaseDataSource", "Failed to get query result documents", it) + throw FirebaseException.ExecuteQueryFailedException(collection = collection.name) + } + } + + protected suspend fun queryDocumentsInRange( + collection: FirebaseCollections, + field: FirebaseDocumentFields, + start: String, + end: String + ): Result { + return runCatching { + fetchCollection(collection) + .whereGreaterThanOrEqualTo(field.name, start) + .whereLessThanOrEqualTo(field.name, end) + .get() + .await() + }.onFailure { + Log.e("FirebaseDataSource", "Failed to get query result documents for range", it) + throw FirebaseException.ExecuteQueryFailedException(collection = collection.name) + } + } + + protected fun streamDocumentsInRange( + collection: FirebaseCollections, + field: FirebaseDocumentFields, + start: String, + end: String, + listener: EventListener + ): ListenerRegistration { + return try { + fetchCollection(collection) + .whereGreaterThanOrEqualTo(field.name, start) + .whereLessThanOrEqualTo(field.name, end) + .addSnapshotListener(listener) + } catch (e: Exception) { + Log.e("FirebaseDataSource", "Failed to get query stream for range", e) + throw FirebaseException.ExecuteQueryFailedException(collection = collection.name) + } + } + + protected suspend fun updateDocument( + collection: FirebaseCollections, + documentId: String, + field: FirebaseDocumentFields, + value: Any + ): Result { + return runCatching { + fetchDocumentReference(collection, documentId).update(field.name, value).await() + }.onFailure { + Log.e("FirebaseDataSource", "Failed to update document", it) + throw FirebaseException.UpdateDocumentFailedException(docId = documentId, collection = collection.name) + } + } + + protected suspend fun setDocument(collection: FirebaseCollections, docId: String, value: Any): Result { + return runCatching { + fetchDocumentReference(collection, docId).set(value).await() + }.onFailure { + Log.e("FirebaseDataSource", "Failed to set document", it) + throw FirebaseException.AddDocumentFailedException(value = value, collection = collection.name) + } + } + + protected suspend fun addDocument(collection: FirebaseCollections, value: Any): Result { + return runCatching { + fetchCollection(collection).add(value).await() + }.onFailure { + Log.e("FirebaseDataSource", "Failed to add document", it) + throw FirebaseException.AddDocumentFailedException(value = value, collection = collection.name) + } + } + + protected suspend fun deleteDocument(document: DocumentReference, collectionName: String = ""): Result { + return runCatching { + document.delete().await() + }.onFailure { exception -> + Log.e("FirebaseDataSource", "Error deleting favorite document with ID: ${document.id}", exception) + throw FirebaseException.DeleteDocumentFailedException(docId = document.id, collection = collectionName) + } + } + + protected suspend fun deleteDocument(collection: FirebaseCollections, documentId: String): Result { + val doc = fetchDocumentReference(collection, documentId) + return deleteDocument(doc, collection.name) + } + + private fun DocumentSnapshot.ifNotExist(action: () -> Unit): DocumentSnapshot { + if (exists().not()) action() + return this + } +} diff --git a/data/firebase/src/main/java/com/squirtles/data/firebase/FirebaseDataSourceConstants.kt b/data/firebase/src/main/java/com/squirtles/data/firebase/FirebaseDataSourceConstants.kt new file mode 100644 index 00000000..f827cd61 --- /dev/null +++ b/data/firebase/src/main/java/com/squirtles/data/firebase/FirebaseDataSourceConstants.kt @@ -0,0 +1,18 @@ +package com.squirtles.data.firebase + +sealed class FirebaseCollections(val name: String) { + data object Favorites: FirebaseCollections("favorites") + data object Picks: FirebaseCollections("picks") + data object Users: FirebaseCollections("users") +} + +sealed class FirebaseDocumentFields(val name: String) { + data object AddedAt: FirebaseDocumentFields("addedAt") + data object PickId: FirebaseDocumentFields("pickId") + data object Uid: FirebaseDocumentFields("uid") + data object MyPicks: FirebaseDocumentFields("myPicks") + data object Name: FirebaseDocumentFields("name") + data object Location: FirebaseDocumentFields("location") + data object GeoHash: FirebaseDocumentFields("geoHash") + data object CreatedUserName: FirebaseDocumentFields("createdBy.userName") +} diff --git a/data/src/main/java/com/squirtles/data/di/FirebaseModule.kt b/data/firebase/src/main/java/com/squirtles/data/firebase/FirebaseModule.kt similarity index 69% rename from data/src/main/java/com/squirtles/data/di/FirebaseModule.kt rename to data/firebase/src/main/java/com/squirtles/data/firebase/FirebaseModule.kt index 53eb74cc..7a466c5e 100644 --- a/data/src/main/java/com/squirtles/data/di/FirebaseModule.kt +++ b/data/firebase/src/main/java/com/squirtles/data/firebase/FirebaseModule.kt @@ -1,7 +1,7 @@ -package com.squirtles.data.di +package com.squirtles.data.firebase import com.google.firebase.firestore.FirebaseFirestore -import com.squirtles.data.BuildConfig +import com.squirtles.core.buildconfig.LocalPropertyProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,6 +15,6 @@ object FirebaseModule { @Provides @Singleton fun provideFirebaseFirestore(): FirebaseFirestore { - return FirebaseFirestore.getInstance(BuildConfig.FIRESTORE_DB_ID) + return FirebaseFirestore.getInstance(LocalPropertyProvider.firestoreDbId) } } diff --git a/data/firebase/src/main/java/com/squirtles/data/firebase/FirebaseRepositoryUtils.kt b/data/firebase/src/main/java/com/squirtles/data/firebase/FirebaseRepositoryUtils.kt new file mode 100644 index 00000000..3646c8db --- /dev/null +++ b/data/firebase/src/main/java/com/squirtles/data/firebase/FirebaseRepositoryUtils.kt @@ -0,0 +1,21 @@ +package com.squirtles.data.firebase + +import com.squirtles.domain.firebase.FirebaseException + +suspend fun handleResult( + firebaseRepositoryException: FirebaseException, + call: suspend () -> T? +): Result { + return runCatching { + call() ?: throw firebaseRepositoryException + } +} + +suspend fun handleResult( + call: suspend () -> T +): Result { + return runCatching { + call() + } +} + diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/model/FirebaseFavorite.kt b/data/firebase/src/main/java/com/squirtles/data/firebase/model/FirebaseFavorite.kt similarity index 79% rename from data/src/main/java/com/squirtles/data/datasource/remote/firebase/model/FirebaseFavorite.kt rename to data/firebase/src/main/java/com/squirtles/data/firebase/model/FirebaseFavorite.kt index 91f659d0..0eff57af 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/model/FirebaseFavorite.kt +++ b/data/firebase/src/main/java/com/squirtles/data/firebase/model/FirebaseFavorite.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.firebase.model +package com.squirtles.data.firebase.model import com.google.firebase.Timestamp import com.google.firebase.firestore.ServerTimestamp diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/model/FirebasePick.kt b/data/firebase/src/main/java/com/squirtles/data/firebase/model/FirebasePick.kt similarity index 94% rename from data/src/main/java/com/squirtles/data/datasource/remote/firebase/model/FirebasePick.kt rename to data/firebase/src/main/java/com/squirtles/data/firebase/model/FirebasePick.kt index 3fb9aab0..d3dbfd21 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/model/FirebasePick.kt +++ b/data/firebase/src/main/java/com/squirtles/data/firebase/model/FirebasePick.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.firebase.model +package com.squirtles.data.firebase.model import com.google.firebase.Timestamp import com.google.firebase.firestore.GeoPoint diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/model/FirebaseUser.kt b/data/firebase/src/main/java/com/squirtles/data/firebase/model/FirebaseUser.kt similarity index 74% rename from data/src/main/java/com/squirtles/data/datasource/remote/firebase/model/FirebaseUser.kt rename to data/firebase/src/main/java/com/squirtles/data/firebase/model/FirebaseUser.kt index 7842a01f..6ab6fe4e 100644 --- a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/model/FirebaseUser.kt +++ b/data/firebase/src/main/java/com/squirtles/data/firebase/model/FirebaseUser.kt @@ -1,4 +1,4 @@ -package com.squirtles.data.datasource.remote.firebase.model +package com.squirtles.data.firebase.model data class FirebaseUser( val email: String? = null, diff --git a/data/src/main/java/com/squirtles/data/mapper/FirebaseMapper.kt b/data/firebase/src/main/java/com/squirtles/data/firebase/model/Mapper.kt similarity index 71% rename from data/src/main/java/com/squirtles/data/mapper/FirebaseMapper.kt rename to data/firebase/src/main/java/com/squirtles/data/firebase/model/Mapper.kt index 82d0dda4..5794987c 100644 --- a/data/src/main/java/com/squirtles/data/mapper/FirebaseMapper.kt +++ b/data/firebase/src/main/java/com/squirtles/data/firebase/model/Mapper.kt @@ -1,26 +1,38 @@ -package com.squirtles.data.mapper +package com.squirtles.data.firebase.model import androidx.core.graphics.toColorInt +import com.squirtles.core.model.Creator +import com.squirtles.core.model.LocationPoint +import com.squirtles.core.model.Pick +import com.squirtles.core.model.Song +import com.squirtles.core.model.User import com.firebase.geofire.GeoFireUtils import com.firebase.geofire.GeoLocation -import com.google.firebase.Timestamp -import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.GeoPoint -import com.squirtles.data.datasource.remote.firebase.model.FirebasePick -import com.squirtles.data.datasource.remote.firebase.model.FirebaseUser -import com.squirtles.domain.model.Creator -import com.squirtles.domain.model.LocationPoint -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.Song -import com.squirtles.domain.model.User import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * using when get pick from firebase and convert to domain data - */ -internal fun FirebasePick.toPick(): Pick = Pick( +fun Pick.toFirebasePick(): FirebasePick = FirebasePick( + id = id, + albumName = song.albumName, + artistName = song.artistName, + artwork = mapOf("url" to song.imageUrl, "bgColor" to song.bgColor.toRgbString()), + comment = comment, + createdBy = mapOf("uid" to createdBy.uid, "userName" to createdBy.userName), + externalUrl = song.externalUrl, + favoriteCount = favoriteCount, + genreNames = song.genreNames, + geoHash = location.toGeoHash(), + location = GeoPoint(location.latitude, location.longitude), + previewUrl = song.previewUrl, + musicVideoUrl = musicVideoUrl, + musicVideoThumbnail = musicVideoThumbnailUrl, + songId = song.id, + songName = song.songName, +) + +fun FirebasePick.toPick(): Pick = Pick( id = id.toString(), song = Song( id = songId.toString(), @@ -50,36 +62,6 @@ internal fun FirebasePick.toPick(): Pick = Pick( musicVideoThumbnailUrl = musicVideoThumbnail ?: "" ) -/** - * using when create pick in firebase - */ -internal fun Pick.toFirebasePick(): FirebasePick = FirebasePick( - id = id, - albumName = song.albumName, - artistName = song.artistName, - artwork = mapOf("url" to song.imageUrl, "bgColor" to song.bgColor.toRgbString()), - comment = comment, - createdBy = mapOf("uid" to createdBy.uid, "userName" to createdBy.userName), - externalUrl = song.externalUrl, - favoriteCount = favoriteCount, - genreNames = song.genreNames, - geoHash = location.toGeoHash(), - location = GeoPoint(location.latitude, location.longitude), - previewUrl = song.previewUrl, - musicVideoUrl = musicVideoUrl, - musicVideoThumbnail = musicVideoThumbnailUrl, - songId = song.id, - songName = song.songName, -) - -internal fun FirebaseUser.toUser(): User = User( - uid = "", - email = email ?: "", - userName = name ?: "", - userProfileImage = profileImage, - myPicks = myPicks -) - private fun Int.toRgbString(): String { return String.format("%06X", 0xFFFFFF and this) } @@ -94,10 +76,10 @@ private fun Date.formatTimestamp(): String { return dateFormat.format(this) } -private fun String.toTimeStamp(): Timestamp { - val dateFormat = SimpleDateFormat("yyyy.MM.dd", Locale.getDefault()) - val date = dateFormat.parse(this) ?: Date() - - val time = FieldValue.serverTimestamp() - return Timestamp(date) -} +fun FirebaseUser.toUser(): User = User( + uid = "", + email = email ?: "", + userName = name ?: "", + userProfileImage = profileImage, + myPicks = myPicks +) diff --git a/data/location/.gitignore b/data/location/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/location/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/location/build.gradle.kts b/data/location/build.gradle.kts new file mode 100644 index 00000000..28ae28ac --- /dev/null +++ b/data/location/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(libs.plugins.musicroad.android.library) + alias(libs.plugins.musicroad.hilt) +} + +android { + namespace = "com.squirtles.data.location" +} + +dependencies { + implementation(projects.domain.location) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) +} diff --git a/data/location/proguard-rules.pro b/data/location/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/data/location/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/location/src/main/java/com/squirtles/data/location/LocalLocationRepositoryImpl.kt b/data/location/src/main/java/com/squirtles/data/location/LocalLocationRepositoryImpl.kt new file mode 100644 index 00000000..e48fc04d --- /dev/null +++ b/data/location/src/main/java/com/squirtles/data/location/LocalLocationRepositoryImpl.kt @@ -0,0 +1,18 @@ +package com.squirtles.data.location + +import android.location.Location +import com.squirtles.domain.location.LocalLocationRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Singleton + +@Singleton +class LocalLocationRepositoryImpl : LocalLocationRepository { + private var _currentLocation: MutableStateFlow = MutableStateFlow(null) + override val lastLocation: StateFlow = _currentLocation.asStateFlow() + + override suspend fun saveCurrentLocation(geoLocation: Location) { + _currentLocation.emit(geoLocation) + } +} diff --git a/data/location/src/main/java/com/squirtles/data/location/di/LocationDiModule.kt b/data/location/src/main/java/com/squirtles/data/location/di/LocationDiModule.kt new file mode 100644 index 00000000..5ec893b3 --- /dev/null +++ b/data/location/src/main/java/com/squirtles/data/location/di/LocationDiModule.kt @@ -0,0 +1,18 @@ +package com.squirtles.data.location.di + +import com.squirtles.domain.location.LocalLocationRepository +import com.squirtles.data.location.LocalLocationRepositoryImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object LocationDiModule { + @Provides + @Singleton + fun provideLocalLocationRepository(): LocalLocationRepository = + LocalLocationRepositoryImpl() +} diff --git a/data/order/.gitignore b/data/order/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/order/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/order/build.gradle.kts b/data/order/build.gradle.kts new file mode 100644 index 00000000..f1e12278 --- /dev/null +++ b/data/order/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id(libs.plugins.musicroad.data.get().pluginId) +} + +android { + namespace = "com.squirtles.data.order" +} + +dependencies { + implementation(projects.domain.order) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) +} diff --git a/data/order/proguard-rules.pro b/data/order/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/data/order/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/order/src/main/java/com/squirtles/data/order/LocalPickListOrderRepositoryImpl.kt b/data/order/src/main/java/com/squirtles/data/order/LocalPickListOrderRepositoryImpl.kt new file mode 100644 index 00000000..a355993a --- /dev/null +++ b/data/order/src/main/java/com/squirtles/data/order/LocalPickListOrderRepositoryImpl.kt @@ -0,0 +1,22 @@ +package com.squirtles.data.order + +import com.squirtles.core.model.Order +import com.squirtles.domain.order.LocalPickListOrderRepository +import javax.inject.Singleton + +@Singleton +class LocalPickListOrderRepositoryImpl : LocalPickListOrderRepository { + private var _favoriteListOrder = Order.LATEST + override val favoriteListOrder get() = _favoriteListOrder + + private var _myListOrder = Order.LATEST + override val myListOrder get() = _myListOrder + + override suspend fun saveFavoriteListOrder(order: Order) { + _favoriteListOrder = order + } + + override suspend fun saveMyListOrder(order: Order) { + _myListOrder = order + } +} diff --git a/data/order/src/main/java/com/squirtles/data/order/di/OrderDiModule.kt b/data/order/src/main/java/com/squirtles/data/order/di/OrderDiModule.kt new file mode 100644 index 00000000..564d1320 --- /dev/null +++ b/data/order/src/main/java/com/squirtles/data/order/di/OrderDiModule.kt @@ -0,0 +1,18 @@ +package com.squirtles.data.order.di + +import com.squirtles.domain.order.LocalPickListOrderRepository +import com.squirtles.data.order.LocalPickListOrderRepositoryImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object OrderDiModule { + @Provides + @Singleton + fun provideLocalPickListOrderRepository(): LocalPickListOrderRepository = + LocalPickListOrderRepositoryImpl() +} diff --git a/data/pick/.gitignore b/data/pick/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/pick/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/pick/build.gradle.kts b/data/pick/build.gradle.kts new file mode 100644 index 00000000..b1a7f20b --- /dev/null +++ b/data/pick/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id(libs.plugins.musicroad.data.get().pluginId) +} + +android { + namespace = "com.squirtles.data.pick" +} + +dependencies { + implementation(projects.domain.pick) + implementation(projects.data.firebase) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + // Firebase + implementation(libs.firebase.firestore.ktx) + implementation(libs.geofire.android.common) +} diff --git a/data/pick/proguard-rules.pro b/data/pick/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/data/pick/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/pick/src/main/java/com/squirtles/data/pick/FirebasePickDataSource.kt b/data/pick/src/main/java/com/squirtles/data/pick/FirebasePickDataSource.kt new file mode 100644 index 00000000..4677c5f6 --- /dev/null +++ b/data/pick/src/main/java/com/squirtles/data/pick/FirebasePickDataSource.kt @@ -0,0 +1,23 @@ +package com.squirtles.data.pick + +import com.squirtles.core.model.Pick +import com.squirtles.data.firebase.model.FirebasePick +import kotlinx.coroutines.flow.Flow + +data class PickWithType( + val type: PickType, + val pick: Pick +) + +enum class PickType { + UPDATED, REMOVED +} + +interface FirebasePickDataSource { + suspend fun fetchPick(pickId: String): Result + suspend fun fetchPicksInArea(lat: Double, lng: Double, radiusInM: Double): Flow> + suspend fun createPick(firebasePick: FirebasePick, userId: String): Result + suspend fun deletePick(pickId: String, userId: String): Result + suspend fun fetchMyPicks(userId: String): Result> + suspend fun fetchFavoritePicks(userId: String): Result> +} diff --git a/data/pick/src/main/java/com/squirtles/data/pick/FirebasePickDataSourceImpl.kt b/data/pick/src/main/java/com/squirtles/data/pick/FirebasePickDataSourceImpl.kt new file mode 100644 index 00000000..c29fb6d5 --- /dev/null +++ b/data/pick/src/main/java/com/squirtles/data/pick/FirebasePickDataSourceImpl.kt @@ -0,0 +1,184 @@ +package com.squirtles.data.pick + +import android.util.Log +import com.firebase.geofire.GeoFireUtils +import com.firebase.geofire.GeoLocation +import com.google.firebase.firestore.DocumentChange +import com.google.firebase.firestore.DocumentReference +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FieldValue +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ListenerRegistration +import com.google.firebase.firestore.toObject +import com.squirtles.data.firebase.BaseFirebaseDataSource +import com.squirtles.data.firebase.FirebaseCollections +import com.squirtles.data.firebase.FirebaseDocumentFields +import com.squirtles.data.firebase.model.FirebaseFavorite +import com.squirtles.data.firebase.model.FirebasePick +import com.squirtles.data.firebase.model.FirebaseUser +import com.squirtles.data.firebase.model.toPick +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.tasks.await +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FirebasePickDataSourceImpl @Inject constructor( + private val db: FirebaseFirestore +) : BaseFirebaseDataSource(db), FirebasePickDataSource { + + /* Fetches a pick by ID from Firestore */ + override suspend fun fetchPick(pickId: String): Result { + return runCatching { + val pickSnap = fetchDocumentSnapshot(FirebaseCollections.Picks, pickId).getOrThrow() + pickSnap.toObject()?.copy(id = pickId)!! + }.onFailure { exception -> + Log.e(TAG_LOG, "Failed to fetch a pick", exception) + } + } + + override suspend fun fetchPicksInArea( + lat: Double, + lng: Double, + radiusInM: Double + ): Flow> = callbackFlow { + val listeners = mutableListOf() + try { + val center = GeoLocation(lat, lng) + val bounds = GeoFireUtils.getGeoHashQueryBounds(center, radiusInM) + + bounds.forEach { bound -> + val listenerRegistration = streamDocumentsInRange( + collection = FirebaseCollections.Picks, + field = FirebaseDocumentFields.GeoHash, + start = bound.startHash, + end = bound.endHash, + ) { snapshot, e -> + if (e != null) { + Log.w("SnapshotListener", "listen:error", e) + throw(e) + } else { + val pickData = snapshot?.documentChanges + ?.filter { isAccurate(it.document, center, radiusInM) } + ?.map { dc -> + val pick = dc.document.toObject().toPick().copy(id = dc.document.id) + when (dc.type) { + DocumentChange.Type.ADDED, DocumentChange.Type.MODIFIED -> PickWithType(PickType.UPDATED, pick) + DocumentChange.Type.REMOVED -> PickWithType(PickType.REMOVED, pick) + } + }.orEmpty() + + trySend(pickData) + } + } + listeners.add(listenerRegistration) + } + } catch (e: Exception) { + close(e) + } + + // Flow 종료 시 모든 리스너 제거 + awaitClose { listeners.forEach { it.remove() } } + } + + /* Creates a new pick in Firestore */ + override suspend fun createPick(firebasePick: FirebasePick, userId: String): Result { + return runCatching { + val pickRef = addDocument(FirebaseCollections.Picks, firebasePick).getOrThrow() + updateCurrentUserPick(userId, pickRef.id) + pickRef.id + }.onFailure { + Log.e(TAG_LOG, "Failed to create a pick", it) + } + } + + override suspend fun deletePick(pickId: String, userId: String): Result { + val pickDocument = fetchDocumentReference(FirebaseCollections.Picks, pickId) + val userDocument = fetchDocumentReference(FirebaseCollections.Users, userId) + + return runCatching { + val favoriteDocuments = fetchFavoriteDocumentRefsByPick(pickId).getOrThrow() + + db.runTransaction { transaction -> + transaction.delete(pickDocument) + favoriteDocuments.forEach { docRef -> + transaction.delete(docRef) + } + transaction.update(userDocument, FirebaseDocumentFields.MyPicks.name, FieldValue.arrayRemove(pickId)) + }.await() + + pickDocument.id + }.onFailure { + Log.e(TAG_LOG, "Failed to delete a pick", it) + } + } + + override suspend fun fetchMyPicks(userId: String): Result> { + return runCatching { + val userDocument = fetchDocumentSnapshot(FirebaseCollections.Users, userId).getOrThrow() + userDocument.toObject()?.myPicks!!.map { + fetchPick(it).getOrThrow() + }.reversed() + } + } + + override suspend fun fetchFavoritePicks(userId: String): Result> { + return runCatching { + val favoriteDocuments = fetchFavoritesByUserId(userId) + favoriteDocuments.map { docSnap -> + fetchPick(docSnap.toObject()?.pickId.toString()).getOrThrow() + } + } + } + + private suspend fun updateCurrentUserPick(userId: String, pickId: String): Result { + return runCatching { + updateDocument( + collection = FirebaseCollections.Users, + documentId = userId, + field = FirebaseDocumentFields.MyPicks, + value = FieldValue.arrayUnion(pickId) + ).getOrThrow() + }.onFailure { e -> + Log.e(TAG_LOG, "Failed to update user picks", e) + } + } + + private suspend fun fetchFavoriteDocumentRefsByPick(pickId: String): Result> { + return runCatching { + queryDocumentsEquals( + collection = FirebaseCollections.Favorites, + fields = listOf(FirebaseDocumentFields.PickId), + values = listOf(pickId), + ).getOrThrow().documents.map { it.reference } + } + } + + /** + * GeoHash의 FP 문제 - Geohash의 쿼리가 정확하지 않으며 클라이언트 측에서 거짓양성 결과를 필터링해야 합니다. + * 이러한 추가 읽기로 인해 앱에 비용과 지연 시간이 추가됩니다. + */ + private fun isAccurate(doc: DocumentSnapshot, center: GeoLocation, radiusInM: Double): Boolean { + val location = doc.getGeoPoint(FirebaseDocumentFields.Location.name) ?: return false + + val docLocation = GeoLocation(location.latitude, location.longitude) + val distanceInM = GeoFireUtils.getDistanceBetween(docLocation, center) + + return distanceInM <= radiusInM + } + + private suspend fun fetchFavoritesByUserId(userId: String): List { + return queryDocumentsEquals( + collection = FirebaseCollections.Favorites, + fields = listOf(FirebaseDocumentFields.Uid), + values = listOf(userId) + ).getOrThrow().documents + } + + companion object { + private const val TAG_LOG = "FirebasePickDataSourceImpl" + } +} + diff --git a/data/pick/src/main/java/com/squirtles/data/pick/FirebasePickRepositoryImpl.kt b/data/pick/src/main/java/com/squirtles/data/pick/FirebasePickRepositoryImpl.kt new file mode 100644 index 00000000..b6f44f7b --- /dev/null +++ b/data/pick/src/main/java/com/squirtles/data/pick/FirebasePickRepositoryImpl.kt @@ -0,0 +1,77 @@ +package com.squirtles.data.pick + +import com.squirtles.core.model.Pick +import com.squirtles.data.firebase.model.toFirebasePick +import com.squirtles.data.firebase.model.toPick +import com.squirtles.domain.pick.FirebasePickRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FirebasePickRepositoryImpl @Inject constructor( + private val pickDataSource: FirebasePickDataSource +) : FirebasePickRepository { + + override suspend fun createPick(pick: Pick): Result { + val firebasePick = pick.toFirebasePick() + return pickDataSource.createPick(firebasePick, pick.createdBy.uid) + } + + override suspend fun deletePick(pickId: String, userId: String): Result { + return pickDataSource.deletePick(pickId, userId) + } + + override suspend fun fetchPick(pickId: String): Result { + return runCatching { + val firebasePick = pickDataSource.fetchPick(pickId).getOrThrow() + firebasePick.toPick() + } + } + + override suspend fun fetchMyPicks(userId: String): Result> { + return runCatching { + val firebasePicks = pickDataSource.fetchMyPicks(userId).getOrThrow() + firebasePicks.map { it.toPick() } + } + } + + override suspend fun fetchPicksInArea( + lat: Double, + lng: Double, + radiusInM: Double + ): Flow> { + val latestNearPick = mutableMapOf() // String : Pick Id + + return pickDataSource.fetchPicksInArea(lat, lng, radiusInM) + .map { pickWithTypeList -> + pickWithTypeList.forEach { pickWithType -> + val pick = pickWithType.pick + when (pickWithType.type) { + PickType.UPDATED -> { + latestNearPick[pick.id] = pick + } + + PickType.REMOVED -> { + latestNearPick[pick.id]?.let { + latestNearPick.remove(pick.id) + } + } + } + } + latestNearPick.values.toList() + } + } + + override suspend fun fetchFavoritePicks(userId: String): Result> { + return runCatching { + val firebasePicks = pickDataSource.fetchFavoritePicks(userId).getOrThrow() + firebasePicks.map { it.toPick() } + } + } + + companion object { + const val TAG_LOG = "FirebasePickRepositoryImpl" + } +} diff --git a/data/pick/src/main/java/com/squirtles/data/pick/di/PickDiModule.kt b/data/pick/src/main/java/com/squirtles/data/pick/di/PickDiModule.kt new file mode 100644 index 00000000..8bc04f8d --- /dev/null +++ b/data/pick/src/main/java/com/squirtles/data/pick/di/PickDiModule.kt @@ -0,0 +1,27 @@ +package com.squirtles.data.pick.di + +import com.squirtles.data.pick.FirebasePickDataSource +import com.squirtles.data.pick.FirebasePickDataSourceImpl +import com.squirtles.domain.pick.FirebasePickRepository +import com.squirtles.data.pick.FirebasePickRepositoryImpl +import com.google.firebase.firestore.FirebaseFirestore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object PickDiModule { + + @Provides + @Singleton + fun provideFirebasePickRepository(firebasePickDataSource: FirebasePickDataSource): FirebasePickRepository = + FirebasePickRepositoryImpl(firebasePickDataSource) + + @Provides + @Singleton + fun provideFirebasePickDataSource(db: FirebaseFirestore): FirebasePickDataSource = + FirebasePickDataSourceImpl(db) +} diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68..00000000 --- a/data/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/FirebaseDataSourceImpl.kt b/data/src/main/java/com/squirtles/data/datasource/remote/firebase/FirebaseDataSourceImpl.kt deleted file mode 100644 index a2d046f4..00000000 --- a/data/src/main/java/com/squirtles/data/datasource/remote/firebase/FirebaseDataSourceImpl.kt +++ /dev/null @@ -1,509 +0,0 @@ -package com.squirtles.data.datasource.remote.firebase - -import android.util.Log -import com.firebase.geofire.GeoFireUtils -import com.firebase.geofire.GeoLocation -import com.google.android.gms.tasks.Task -import com.google.android.gms.tasks.Tasks -import com.google.firebase.firestore.DocumentChange -import com.google.firebase.firestore.DocumentReference -import com.google.firebase.firestore.DocumentSnapshot -import com.google.firebase.firestore.FieldValue -import com.google.firebase.firestore.FirebaseFirestore -import com.google.firebase.firestore.ListenerRegistration -import com.google.firebase.firestore.Query -import com.google.firebase.firestore.QuerySnapshot -import com.google.firebase.firestore.toObject -import com.squirtles.data.datasource.remote.firebase.model.FirebaseFavorite -import com.squirtles.data.datasource.remote.firebase.model.FirebasePick -import com.squirtles.data.datasource.remote.firebase.model.FirebaseUser -import com.squirtles.data.mapper.toFirebasePick -import com.squirtles.data.mapper.toPick -import com.squirtles.data.mapper.toUser -import com.squirtles.domain.firebase.FirebaseRemoteDataSource -import com.squirtles.domain.firebase.PickType -import com.squirtles.domain.firebase.PickWithType -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.User -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.tasks.await -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -@Singleton -class FirebaseDataSourceImpl @Inject constructor( - private val db: FirebaseFirestore -) : FirebaseRemoteDataSource { - - private val cloudFunctionHelper = CloudFunctionHelper() - - override suspend fun createGoogleIdUser( - uid: String, - email: String, - userName: String?, - userProfileImage: String? - ): User? { - return suspendCancellableCoroutine { continuation -> - val documentReference = db.collection("users").document(uid) - documentReference.set( - FirebaseUser( - email = email, - name = userName, - profileImage = userProfileImage - ) - ) - .addOnSuccessListener { - documentReference.get() - .addOnSuccessListener { documentSnapshot -> - val savedUser = documentSnapshot.toObject() - continuation.resume( - savedUser?.toUser()?.copy(uid = documentReference.id) - ) - } - .addOnFailureListener { exception -> - continuation.resumeWithException(exception) - } - } - .addOnFailureListener { exception -> - Log.e("FirebaseDataSourceImpl", exception.message.toString()) - continuation.resumeWithException(exception) - } - } - } - - override suspend fun fetchUser(uid: String): User? { - return suspendCancellableCoroutine { continuation -> - db.collection("users").document(uid).get() - .addOnSuccessListener { document -> - val firebaseUser = document.toObject() - continuation.resume(firebaseUser?.toUser()?.copy(uid = uid)) - } - .addOnFailureListener { exception -> - continuation.resumeWithException(exception) - } - } - } - - override suspend fun updateUserName(uid: String, newUserName: String): Boolean { - return suspendCancellableCoroutine { continuation -> - db.runTransaction { transaction -> - val userRef = db.collection("users").document(uid) - val userDocument = transaction.get(userRef) - transaction.update(userRef, "name", newUserName) - - val myPicks = userDocument.get("myPicks")?.let { it as List } ?: emptyList() - myPicks.forEach { pickId -> - val pickRef = db.collection("picks").document(pickId) - transaction.update(pickRef, "createdBy.userName", newUserName) - } - }.addOnSuccessListener { - continuation.resume(true) - }.addOnFailureListener { exception -> - continuation.resumeWithException(exception) - } - } - } - - override suspend fun deleteUser(uid: String): Boolean { - return suspendCancellableCoroutine { continuation -> - db.collection("users").document(uid).delete() - .addOnSuccessListener { continuation.resume(true) } - .addOnFailureListener { exception -> continuation.resumeWithException(exception) } - } - } - - /** - * Fetches a pick by ID from Firestore. - * @param pickID The ID of the pick to fetch. - * @return The fetched pick, or null if the pick does not exist on firestore. - */ - override suspend fun fetchPick(pickID: String): Pick? { - return suspendCancellableCoroutine { continuation -> - db.collection("picks").document(pickID).get() - .addOnSuccessListener { document -> - val firestorePick = document.toObject()?.copy(id = pickID) - val resultPick = firestorePick?.toPick() - continuation.resume(resultPick) - } - .addOnFailureListener { exception -> - Log.e("FirebaseDataSourceImpl", "Failed to fetch a pick", exception) - continuation.resumeWithException(exception) - } - } - } - - /** - * Fetches picks within a given radius from Firestore. - * @param lat The latitude of the center of the search area. - * @param lng The longitude of the center of the search area. - * @param radiusInM The radius in meters of the search area. - * @return A list of picks within the specified radius, ordered by distance from the center. can be empty. - */ - override suspend fun fetchPicksInArea( - lat: Double, - lng: Double, - radiusInM: Double - ): Flow> = callbackFlow { - val listeners = mutableListOf() - try { - val center = GeoLocation(lat, lng) - val bounds = GeoFireUtils.getGeoHashQueryBounds(center, radiusInM) - - bounds.forEach { bound -> - val query = db.collection(COLLECTION_PICKS) - .orderBy("geoHash") - .startAt(bound.startHash) - .endAt(bound.endHash) - - val listener = query.addSnapshotListener { snapshots, e -> - if (e != null) { - Log.w("SnapshotListener", "listen:error", e) - return@addSnapshotListener - } - - val pickData = mutableListOf() - for (dc in snapshots!!.documentChanges) { - if (isAccurate(dc.document, center, radiusInM)) { - dc.document.toObject().run { - when (dc.type) { - DocumentChange.Type.ADDED, DocumentChange.Type.MODIFIED -> { - pickData.add( - PickWithType( - type = PickType.UPDATED, - pick = this.toPick().copy(id = dc.document.id) - ) - ) - } - - DocumentChange.Type.REMOVED -> { - pickData.add( - PickWithType( - type = PickType.REMOVED, - pick = this.toPick().copy(id = dc.document.id) - ) - ) - } - } - } - } - } - trySend(pickData) - } - listeners.add(listener) - } - } catch (e: Exception) { - close(e) - } - - // Flow 종료 시 모든 리스너 제거 - awaitClose { listeners.forEach { it.remove() } } - } - - /** - * GeoHash의 FP 문제 - Geohash의 쿼리가 정확하지 않으며 클라이언트 측에서 거짓양성 결과를 필터링해야 합니다. - * 이러한 추가 읽기로 인해 앱에 비용과 지연 시간이 추가됩니다. - * @param doc The pick document to check. - * @param center The center of the search area. - * @param radiusInM The radius in meters of the search area. - * @return True if the pick is within the specified radius, false otherwise. - */ - private fun isAccurate(doc: DocumentSnapshot, center: GeoLocation, radiusInM: Double): Boolean { - val location = doc.getGeoPoint("location") ?: return false - - val docLocation = GeoLocation(location.latitude, location.longitude) - val distanceInM = GeoFireUtils.getDistanceBetween(docLocation, center) - - return distanceInM <= radiusInM - } - - /** - * Creates a new pick in Firestore. - * @param pick The pick to create. - * @return The created pick. - */ - override suspend fun createPick(pick: Pick): String = - suspendCancellableCoroutine { continuation -> - val firebasePick = pick.toFirebasePick() - - // add() 메소드는 Cloud Firestore에서 ID를 자동으로 생성 - db.collection("picks").add(firebasePick) - .addOnSuccessListener { documentReference -> - val pickId = documentReference.id - // 유저의 픽 정보 업데이트 - updateCurrentUserPick(pick.createdBy.uid, pickId) - .addOnCompleteListener { task -> - if (task.isSuccessful) { - continuation.resume(pickId) - } else { - continuation.resumeWithException( - task.exception ?: Exception("Failed to updating user pick info") - ) - } - } - } - .addOnFailureListener { exception -> - Log.e("FirebaseDataSourceImpl", "Failed to create a pick", exception) - continuation.resumeWithException(exception) - } - } - - override suspend fun deletePick(pickId: String, uid: String): Boolean { - val pickDocument = db.collection(COLLECTION_PICKS).document(pickId) - val userDocument = db.collection(COLLECTION_USERS).document(uid) - val favoriteDocuments = fetchFavoriteDocuments(pickId) - - return suspendCancellableCoroutine { continuation -> - db.runTransaction { transaction -> - transaction.delete(pickDocument) - - favoriteDocuments.forEach { document -> - transaction.delete(document) - } - - transaction.update(userDocument, FIELD_MY_PICKS, FieldValue.arrayRemove(pickId)) - }.addOnSuccessListener { _ -> - continuation.resume(true) - }.addOnFailureListener { e -> - Log.w(TAG_LOG, "Transaction failure.", e) - continuation.resumeWithException(e) - } - } - } - - override suspend fun fetchMyPicks(uid: String): List { - val userDocument = fetchUserDocument(uid) - if (userDocument.exists().not()) throw Exception("No user info in database") - - val tasks = mutableListOf>() - val myPicks = mutableListOf() - - try { - userDocument.toObject()?.myPicks?.forEach { pickId -> - tasks.add( - db.collection(COLLECTION_PICKS) - .document(pickId) - .get() - ) - } - Tasks.whenAllComplete(tasks).await() - } catch (exception: Exception) { - Log.e("FirebaseDataSourceImpl", "Failed to fetch my picks", exception) - throw exception - } - - tasks.forEach { task -> - task.result.toObject()?.run { - myPicks.add(this.toPick().copy(id = task.result.id)) - } - } - - return myPicks.reversed() - } - - override suspend fun fetchFavoritePicks(uid: String): List { - val favoriteDocuments = fetchFavoritesByUid(uid) - - val tasks = mutableListOf>() - val favorites = mutableListOf() - - try { - favoriteDocuments.forEach { doc -> - tasks.add( - db.collection(COLLECTION_PICKS) - .document(doc.data[FIELD_PICK_ID].toString()) - .get() - ) - } - Tasks.whenAllComplete(tasks).await() - } catch (exception: Exception) { - Log.e("FirebaseDataSourceImpl", "Failed to get favorite picks", exception) - throw exception - } - tasks.forEach { task -> - task.result.toObject()?.run { - favorites.add(this.toPick().copy(id = task.result.id)) - } - } - - return favorites - } - - override suspend fun fetchIsFavorite(pickId: String, uid: String): Boolean { - val favoriteDocument = fetchFavoriteByPickIdAndUid(pickId, uid) - return favoriteDocument.isEmpty.not() - } - - override suspend fun createFavorite(pickId: String, uid: String): Boolean { - return suspendCancellableCoroutine { continuation -> - val firebaseFavorite = FirebaseFavorite( - pickId = pickId, - uid = uid - ) - - db.collection(COLLECTION_FAVORITES) - .add(firebaseFavorite) - .addOnSuccessListener { - // favorites에 문서 생성 후 클라우드 함수가 완료됐을 때 담기 완료 - CoroutineScope(Dispatchers.IO).launch { - try { - updateFavoriteCount(pickId) // 클라우드 함수 호출 - continuation.resume(true) - } catch (e: Exception) { - continuation.resumeWithException(e) - } - } - } - .addOnFailureListener { exception -> - Log.e("FirebaseDataSourceImpl", "Failed to create favorite", exception) - continuation.resumeWithException(exception) - } - } - } - - override suspend fun deleteFavorite(pickId: String, uid: String): Boolean { - val favoriteDocument = fetchFavoriteByPickIdAndUid(pickId, uid) - return suspendCancellableCoroutine { continuation -> - favoriteDocument.forEach { document -> - db.collection(COLLECTION_FAVORITES).document(document.id) - .delete() - .addOnSuccessListener { - // favorites에 문서 삭제 후 클라우드 함수가 완료됐을 때 담기 해제 완료 - CoroutineScope(Dispatchers.IO).launch { - try { - updateFavoriteCount(pickId) // 클라우드 함수 호출 - continuation.resume(true) - } catch (e: Exception) { - continuation.resumeWithException(e) - } - } - } - .addOnFailureListener { exception -> - Log.w( - "FirebaseDataSourceImpl", - "Error deleting favorite document", - exception - ) - continuation.resumeWithException(exception) - } - } - } - } - - private fun updateCurrentUserPick(uid: String, pickId: String): Task { - val userDoc = db.collection("users").document(uid) - return userDoc.update("myPicks", FieldValue.arrayUnion(pickId)) - } - - private suspend fun fetchFavoriteByPickIdAndUid( - pickId: String, - uid: String - ): QuerySnapshot { - return suspendCancellableCoroutine { continuation -> - db.collection(COLLECTION_FAVORITES) - .whereEqualTo(FIELD_PICK_ID, pickId) - .whereEqualTo(FIELD_USER_ID, uid) - .get() - .addOnSuccessListener { result -> - continuation.resume(result) - } - .addOnFailureListener { exception -> - Log.w( - "FirebaseDataSourceImpl", - "Error at fetching favorite document", - exception - ) - continuation.resumeWithException(exception) - } - } - } - - private suspend fun fetchFavoritesByUid(uid: String): QuerySnapshot { - return suspendCancellableCoroutine { continuation -> - db.collection(COLLECTION_FAVORITES) - .whereEqualTo(FIELD_USER_ID, uid) - .orderBy(FIELD_ADDED_AT, Query.Direction.DESCENDING) - .get() - .addOnSuccessListener { result -> - continuation.resume(result) - } - .addOnFailureListener { exception -> - Log.w( - "FirebaseDataSourceImpl", - "Error at fetching favorite documents", - exception - ) - continuation.resumeWithException(exception) - } - } - } - - private suspend fun fetchUserDocument(uid: String): DocumentSnapshot { - return suspendCancellableCoroutine { continuation -> - db.collection(COLLECTION_USERS).document(uid) - .get() - .addOnSuccessListener { document -> - continuation.resume(document) - } - .addOnFailureListener { exception -> - Log.e("FirebaseDataSourceImpl", "Failed to get user document", exception) - continuation.resumeWithException(exception) - } - } - } - - private suspend fun updateFavoriteCount(pickId: String) { - try { - val result = cloudFunctionHelper.updateFavoriteCount(pickId) - result.onSuccess { - Log.d("FirebaseDataSourceImpl", "Success to update favorite count") - }.onFailure { exception -> - Log.e("FirebaseDataSourceImpl", "Failed to update favorite count", exception) - throw exception - } - } catch (e: Exception) { - Log.e("FirebaseDataSourceImpl", "Exception occurred while updating favorite count", e) - throw e - } - } - - private suspend fun fetchFavoriteDocuments(pickId: String): List { - return suspendCancellableCoroutine { continuation -> - db.collection(COLLECTION_FAVORITES) - .whereEqualTo(FIELD_PICK_ID, pickId) - .get() - .addOnSuccessListener { querySnapShot -> - val documentIds = querySnapShot.documents.map { it.id } - val documentRefs = mutableListOf() - documentIds.forEach { id -> - documentRefs.add(db.collection(COLLECTION_FAVORITES).document(id)) - } - continuation.resume(documentRefs) - } - .addOnFailureListener { e -> - Log.w(TAG_LOG, "Failed to fetch favorite documents id", e) - continuation.resumeWithException(e) - } - } - } - - companion object { - private const val TAG_LOG = "FirebaseDataSourceImpl" - - private const val COLLECTION_FAVORITES = "favorites" - private const val COLLECTION_PICKS = "picks" - private const val COLLECTION_USERS = "users" - - private const val FIELD_PICK_ID = "pickId" - private const val FIELD_USER_ID = "uid" - private const val FIELD_ADDED_AT = "addedAt" - private const val FIELD_MY_PICKS = "myPicks" - } -} diff --git a/data/src/main/java/com/squirtles/data/di/DataModule.kt b/data/src/main/java/com/squirtles/data/di/DataModule.kt deleted file mode 100644 index e3363344..00000000 --- a/data/src/main/java/com/squirtles/data/di/DataModule.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.squirtles.data.di - -import android.content.Context -import com.google.firebase.firestore.FirebaseFirestore -import com.squirtles.data.datasource.local.LocalDataSourceImpl -import com.squirtles.data.datasource.remote.applemusic.AppleMusicDataSourceImpl -import com.squirtles.data.datasource.remote.applemusic.api.AppleMusicApi -import com.squirtles.data.datasource.remote.firebase.FirebaseDataSourceImpl -import com.squirtles.data.repository.AppleMusicRepositoryImpl -import com.squirtles.data.repository.FirebaseRepositoryImpl -import com.squirtles.data.repository.LocalRepositoryImpl -import com.squirtles.domain.applemusic.AppleMusicRemoteDataSource -import com.squirtles.domain.firebase.FirebaseRemoteDataSource -import com.squirtles.domain.local.LocalDataSource -import com.squirtles.domain.applemusic.AppleMusicRepository -import com.squirtles.domain.firebase.FirebaseRepository -import com.squirtles.domain.local.LocalRepository -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object DataModule { - - @Provides - @Singleton - fun provideLocalRepository(localDataSource: LocalDataSource): LocalRepository = - LocalRepositoryImpl(localDataSource) - - @Provides - @Singleton - fun provideLocalDataSource(@ApplicationContext context: Context): LocalDataSource = - LocalDataSourceImpl(context) - - @Provides - @Singleton - fun provideFirebaseRepository(firebaseRemoteDataSource: FirebaseRemoteDataSource): FirebaseRepository = - FirebaseRepositoryImpl(firebaseRemoteDataSource) - - @Provides - @Singleton - fun provideFirebaseRemoteDataSource(db: FirebaseFirestore): FirebaseRemoteDataSource = - FirebaseDataSourceImpl(db) - - @Provides - @Singleton - fun provideAppleMusicRepository(appleMusicDataSource: AppleMusicRemoteDataSource): AppleMusicRepository = - AppleMusicRepositoryImpl(appleMusicDataSource) - - @Provides - @Singleton - fun provideAppleMusicDataSource(api: AppleMusicApi): AppleMusicRemoteDataSource = - AppleMusicDataSourceImpl(api) -} diff --git a/data/src/main/java/com/squirtles/data/repository/FirebaseRepositoryImpl.kt b/data/src/main/java/com/squirtles/data/repository/FirebaseRepositoryImpl.kt deleted file mode 100644 index 96706ee7..00000000 --- a/data/src/main/java/com/squirtles/data/repository/FirebaseRepositoryImpl.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.squirtles.data.repository - -import android.util.Log -import com.squirtles.domain.firebase.FirebaseRemoteDataSource -import com.squirtles.domain.firebase.FirebaseException -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.User -import com.squirtles.domain.firebase.FirebaseRepository -import com.squirtles.domain.firebase.PickType -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class FirebaseRepositoryImpl @Inject constructor( - private val firebaseRemoteDataSource: FirebaseRemoteDataSource -) : FirebaseRepository { - - override suspend fun createGoogleIdUser( - uid: String, - email: String, - userName: String?, - userProfileImage: String? - ): Result { - return handleResult(FirebaseException.CreatedUserFailedException()) { - firebaseRemoteDataSource.createGoogleIdUser(uid, email, userName, userProfileImage) - } - } - - override suspend fun fetchUser(uid: String): Result { - return handleResult(FirebaseException.UserNotFoundException()) { - firebaseRemoteDataSource.fetchUser(uid) - } - } - - override suspend fun updateUserName(uid: String, newUserName: String): Result { - return handleResult { - firebaseRemoteDataSource.updateUserName(uid, newUserName) - } - } - - override suspend fun deleteUser(uid: String): Result { - return handleResult(FirebaseException.UserNotFoundException()) { - firebaseRemoteDataSource.deleteUser(uid) - } - } - - override suspend fun fetchPick(pickID: String): Result { - return handleResult(FirebaseException.NoSuchPickException()) { - firebaseRemoteDataSource.fetchPick(pickID) - } - } - - override suspend fun fetchPicksInArea( - lat: Double, - lng: Double, - radiusInM: Double - ): Flow> { - val latestNearPick = mutableMapOf() // String : Pick Id - - return firebaseRemoteDataSource.fetchPicksInArea(lat, lng, radiusInM) - .map { pickWithTypeList -> - pickWithTypeList.forEach { pickWithType -> - val pick = pickWithType.pick - when (pickWithType.type) { - PickType.UPDATED -> { - latestNearPick[pick.id] = pick - } - - PickType.REMOVED -> { - latestNearPick[pick.id]?.let { - latestNearPick.remove(pick.id) - } - } - } - } - latestNearPick.values.toList() - } - } - - override suspend fun createPick(pick: Pick): Result { - return handleResult { - firebaseRemoteDataSource.createPick(pick) - } - } - - override suspend fun deletePick(pickId: String, uid: String): Result { - return handleResult { - firebaseRemoteDataSource.deletePick(pickId, uid) - } - } - - override suspend fun fetchMyPicks(uid: String): Result> { - return handleResult { - firebaseRemoteDataSource.fetchMyPicks(uid) - } - } - - override suspend fun fetchFavoritePicks(uid: String): Result> { - return handleResult { - firebaseRemoteDataSource.fetchFavoritePicks(uid) - } - } - - override suspend fun fetchIsFavorite(pickId: String, uid: String): Result { - return handleResult { - firebaseRemoteDataSource.fetchIsFavorite(pickId, uid) - } - } - - override suspend fun createFavorite(pickId: String, uid: String): Result { - return handleResult { - firebaseRemoteDataSource.createFavorite(pickId, uid) - } - } - - override suspend fun deleteFavorite(pickId: String, uid: String): Result { - return handleResult { - firebaseRemoteDataSource.deleteFavorite(pickId, uid) - } - } - - private suspend fun handleResult( - firebaseRepositoryException: FirebaseException, - call: suspend () -> T? - ): Result { - return runCatching { - call() ?: throw firebaseRepositoryException - } - } - - private suspend fun handleResult( - call: suspend () -> T - ): Result { - return runCatching { - call() - } - } -} diff --git a/data/src/main/java/com/squirtles/data/repository/LocalRepositoryImpl.kt b/data/src/main/java/com/squirtles/data/repository/LocalRepositoryImpl.kt deleted file mode 100644 index 722944a9..00000000 --- a/data/src/main/java/com/squirtles/data/repository/LocalRepositoryImpl.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.squirtles.data.repository - -import android.location.Location -import com.squirtles.domain.local.LocalDataSource -import com.squirtles.domain.local.LocalRepository -import com.squirtles.domain.model.Order -import com.squirtles.domain.model.User -import kotlinx.coroutines.flow.Flow -import javax.inject.Inject - -class LocalRepositoryImpl @Inject constructor( - private val localDataSource: LocalDataSource, -) : LocalRepository { - override val lastLocation get() = localDataSource.lastLocation - override val favoriteListOrder get() = localDataSource.favoriteListOrder - override val myListOrder get() = localDataSource.myListOrder - - override fun readUidDataStore(): Flow { - return localDataSource.readUidDataStore() - } - - override suspend fun clearUser(): Result { - return try { - localDataSource.clearUser() - Result.success(Unit) - } catch (e: Exception) { - Result.failure(e) - } - } - - override suspend fun saveCurrentLocation(location: Location) { - localDataSource.saveCurrentLocation(location) - } - - override suspend fun saveFavoriteListOrder(order: Order) { - localDataSource.saveFavoriteListOrder(order) - } - - override suspend fun saveMyListOrder(order: Order) { - localDataSource.saveMyListOrder(order) - } -} diff --git a/data/user/.gitignore b/data/user/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/user/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/user/build.gradle.kts b/data/user/build.gradle.kts new file mode 100644 index 00000000..03b82a06 --- /dev/null +++ b/data/user/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id(libs.plugins.musicroad.data.get().pluginId) +} + +android { + namespace = "com.squirtles.data.user" +} + +dependencies { + implementation(projects.domain.user) + implementation(projects.data.firebase) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + // Datastore + implementation(libs.androidx.datastore.preferences) + + // firebase + implementation(libs.firebase.firestore.ktx) + implementation(libs.firebase.auth.ktx) +} diff --git a/data/user/proguard-rules.pro b/data/user/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/data/user/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/user/src/main/java/com/squirtles/data/user/FirebaseUserDataSource.kt b/data/user/src/main/java/com/squirtles/data/user/FirebaseUserDataSource.kt new file mode 100644 index 00000000..38a43967 --- /dev/null +++ b/data/user/src/main/java/com/squirtles/data/user/FirebaseUserDataSource.kt @@ -0,0 +1,10 @@ +package com.squirtles.data.user + +import com.squirtles.data.firebase.model.FirebaseUser + +interface FirebaseUserDataSource { + suspend fun fetchUser(uid: String): Result + suspend fun createGoogleIdUser(uid: String, newUser: FirebaseUser): Result + suspend fun updateUserName(uid: String, newUserName: String): Result + suspend fun deleteUser(uid: String): Result +} diff --git a/data/user/src/main/java/com/squirtles/data/user/FirebaseUserDataSourceImpl.kt b/data/user/src/main/java/com/squirtles/data/user/FirebaseUserDataSourceImpl.kt new file mode 100644 index 00000000..3857798c --- /dev/null +++ b/data/user/src/main/java/com/squirtles/data/user/FirebaseUserDataSourceImpl.kt @@ -0,0 +1,69 @@ +package com.squirtles.data.user + +import android.util.Log +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.toObject +import com.squirtles.data.firebase.BaseFirebaseDataSource +import com.squirtles.data.firebase.FirebaseCollections +import com.squirtles.data.firebase.FirebaseDocumentFields +import com.squirtles.data.firebase.model.FirebaseUser +import kotlinx.coroutines.tasks.await +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FirebaseUserDataSourceImpl @Inject constructor( + private val db: FirebaseFirestore +) : BaseFirebaseDataSource(db), FirebaseUserDataSource { + + override suspend fun createGoogleIdUser(uid: String, newUser: FirebaseUser): Result { + return runCatching { + setDocument(FirebaseCollections.Users, uid, newUser) + newUser + }.onFailure { e -> + Log.e(TAG_LOG, e.message.toString()) + } + } + + override suspend fun fetchUser(uid: String): Result { + return runCatching { + val userDocSnap = fetchDocumentSnapshot(FirebaseCollections.Users, uid).getOrThrow() + userDocSnap.toObject()!! + }.onFailure { e -> + Log.e(TAG_LOG, "Failed to fetch a user", e) + } + } + + override suspend fun updateUserName(uid: String, newUserName: String): Result { + return runCatching { + val userDocSnap = fetchDocumentSnapshot(FirebaseCollections.Users, uid).getOrThrow() + db.runTransaction { transaction -> + transaction.update(userDocSnap.reference, FirebaseDocumentFields.Name.name, newUserName) + + val myPicks = userDocSnap.toObject()?.myPicks + requireNotNull(myPicks) + myPicks.forEach { pickId -> + val pickRef = fetchDocumentReference(FirebaseCollections.Picks, pickId) + transaction.update(pickRef, FirebaseDocumentFields.CreatedUserName.name, newUserName) + } + } + true + }.onFailure { e -> + Log.e(TAG_LOG, "Failed to update a user name", e) + } + } + + override suspend fun deleteUser(uid: String): Result { + return runCatching { + FirebaseAuth.getInstance().currentUser?.delete()?.await() + return deleteDocument(FirebaseCollections.Users, uid) + }.onFailure { + Log.e(TAG_LOG, "Failed to delete a user", it) + } + } + + companion object { + const val TAG_LOG = "FirebaseUserDataSourceImpl" + } +} diff --git a/data/user/src/main/java/com/squirtles/data/user/FirebaseUserRepositoryImpl.kt b/data/user/src/main/java/com/squirtles/data/user/FirebaseUserRepositoryImpl.kt new file mode 100644 index 00000000..99ed2bf9 --- /dev/null +++ b/data/user/src/main/java/com/squirtles/data/user/FirebaseUserRepositoryImpl.kt @@ -0,0 +1,47 @@ +package com.squirtles.data.user + +import com.google.firebase.auth.FirebaseAuth +import com.squirtles.data.firebase.model.FirebaseUser +import com.squirtles.data.firebase.model.toUser +import com.squirtles.core.model.User +import com.squirtles.domain.user.FirebaseUserRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FirebaseUserRepositoryImpl @Inject constructor( + private val userDataSource: FirebaseUserDataSource +) : FirebaseUserRepository { + + override val currentUser get() = FirebaseAuth.getInstance().currentUser?.uid + + override fun signOut() = FirebaseAuth.getInstance().signOut() + + override suspend fun createGoogleIdUser( + uid: String, + email: String, + userName: String?, + userProfileImage: String? + ): Result { + return runCatching { + val newUser = FirebaseUser(email = email, name = userName, profileImage = userProfileImage) + val firebaseUser = userDataSource.createGoogleIdUser(uid, newUser).getOrThrow() + firebaseUser.toUser().copy(uid = uid) + } + } + + override suspend fun fetchUser(userId: String): Result { + return runCatching{ + val firebaseUser = userDataSource.fetchUser(userId).getOrThrow() + firebaseUser.toUser().copy(uid = userId) + } + } + + override suspend fun updateUserName(userId: String, newUserName: String): Result { + return userDataSource.updateUserName(userId, newUserName) + } + + override suspend fun deleteUser(uid: String): Result { + return userDataSource.deleteUser(uid) + } +} diff --git a/data/user/src/main/java/com/squirtles/data/user/LocalUserDataSource.kt b/data/user/src/main/java/com/squirtles/data/user/LocalUserDataSource.kt new file mode 100644 index 00000000..771a9bfe --- /dev/null +++ b/data/user/src/main/java/com/squirtles/data/user/LocalUserDataSource.kt @@ -0,0 +1,13 @@ +package com.squirtles.data.user + +import com.squirtles.core.model.User +import kotlinx.coroutines.flow.Flow + +interface LocalUserDataSource { + val currentUser: User? + + fun readUserIdDataStore(): Flow + suspend fun saveUserIdDataStore(userId: String) + suspend fun saveCurrentUser(user: User) + suspend fun clearUser() +} diff --git a/data/src/main/java/com/squirtles/data/datasource/local/LocalDataSourceImpl.kt b/data/user/src/main/java/com/squirtles/data/user/LocalUserDataSourceImpl.kt similarity index 51% rename from data/src/main/java/com/squirtles/data/datasource/local/LocalDataSourceImpl.kt rename to data/user/src/main/java/com/squirtles/data/user/LocalUserDataSourceImpl.kt index 3d9ccdfc..bd9cfe57 100644 --- a/data/src/main/java/com/squirtles/data/datasource/local/LocalDataSourceImpl.kt +++ b/data/user/src/main/java/com/squirtles/data/user/LocalUserDataSourceImpl.kt @@ -1,49 +1,36 @@ -package com.squirtles.data.datasource.local +package com.squirtles.data.user import android.content.Context -import android.location.Location import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import com.squirtles.domain.local.LocalDataSource -import com.squirtles.domain.model.Order -import com.squirtles.domain.model.User +import com.squirtles.core.model.User import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import javax.inject.Inject +import javax.inject.Singleton -class LocalDataSourceImpl @Inject constructor( +@Singleton +class LocalUserDataSourceImpl @Inject constructor( private val context: Context, -) : LocalDataSource { +) : LocalUserDataSource { private val Context.dataStore by preferencesDataStore(name = USER_PREFERENCES_NAME) private var _currentUser: User? = null override val currentUser: User? get() = _currentUser - private var _currentLocation: MutableStateFlow = MutableStateFlow(null) - override val lastLocation: StateFlow = _currentLocation.asStateFlow() - - private var _favoriteListOrder = Order.LATEST - override val favoriteListOrder get() = _favoriteListOrder - - private var _myListOrder = Order.LATEST - override val myListOrder get() = _myListOrder - - override fun readUidDataStore(): Flow { + override fun readUserIdDataStore(): Flow { val dataStoreKey = stringPreferencesKey(USER_ID_KEY) return context.dataStore.data.map { preferences -> preferences[dataStoreKey] } } - override suspend fun saveUidDataStore(uid: String) { + override suspend fun saveUserIdDataStore(userId: String) { val dataStoreKey = stringPreferencesKey(USER_ID_KEY) context.dataStore.edit { preferences -> - preferences[dataStoreKey] = uid + preferences[dataStoreKey] = userId } } @@ -59,18 +46,6 @@ class LocalDataSourceImpl @Inject constructor( _currentUser = null } - override suspend fun saveCurrentLocation(location: Location) { - _currentLocation.emit(location) - } - - override suspend fun saveFavoriteListOrder(order: Order) { - _favoriteListOrder = order - } - - override suspend fun saveMyListOrder(order: Order) { - _myListOrder = order - } - companion object { private const val USER_PREFERENCES_NAME = "user_preferences" private const val USER_ID_KEY = "user_id" diff --git a/data/user/src/main/java/com/squirtles/data/user/LocalUserRepositoryImpl.kt b/data/user/src/main/java/com/squirtles/data/user/LocalUserRepositoryImpl.kt new file mode 100644 index 00000000..8432097d --- /dev/null +++ b/data/user/src/main/java/com/squirtles/data/user/LocalUserRepositoryImpl.kt @@ -0,0 +1,35 @@ +package com.squirtles.data.user + +import com.squirtles.core.model.User +import com.squirtles.domain.user.LocalUserRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocalUserRepositoryImpl @Inject constructor( + private val userDataSource: LocalUserDataSource +) : LocalUserRepository { + override val currentUser get() = userDataSource.currentUser + + override fun readUserIdDataStore(): Flow { + return userDataSource.readUserIdDataStore() + } + + override suspend fun saveUserIdDataStore(userId: String) { + userDataSource.saveUserIdDataStore(userId) + } + + override suspend fun saveCurrentUser(user: User) { + userDataSource.saveCurrentUser(user) + } + + override suspend fun clearUser(): Result { + return try { + userDataSource.clearUser() + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } +} diff --git a/data/user/src/main/java/com/squirtles/data/user/di/UserDiModule.kt b/data/user/src/main/java/com/squirtles/data/user/di/UserDiModule.kt new file mode 100644 index 00000000..f6a8b4e8 --- /dev/null +++ b/data/user/src/main/java/com/squirtles/data/user/di/UserDiModule.kt @@ -0,0 +1,42 @@ +package com.squirtles.data.user.di + +import android.content.Context +import com.google.firebase.firestore.FirebaseFirestore +import com.squirtles.data.user.FirebaseUserDataSource +import com.squirtles.data.user.FirebaseUserDataSourceImpl +import com.squirtles.domain.user.FirebaseUserRepository +import com.squirtles.data.user.FirebaseUserRepositoryImpl +import com.squirtles.data.user.LocalUserDataSource +import com.squirtles.data.user.LocalUserDataSourceImpl +import com.squirtles.domain.user.LocalUserRepository +import com.squirtles.data.user.LocalUserRepositoryImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object UserDiModule { + @Provides + @Singleton + fun provideLocalUserRepository(localUserDataSource: LocalUserDataSource): LocalUserRepository = + LocalUserRepositoryImpl(localUserDataSource) + + @Provides + @Singleton + fun provideLocalUserDataSource(@ApplicationContext context: Context): LocalUserDataSource = + LocalUserDataSourceImpl(context) + + @Provides + @Singleton + fun provideFirebaseUserRepository(firebaseUserDataSource: FirebaseUserDataSource): FirebaseUserRepository = + FirebaseUserRepositoryImpl(firebaseUserDataSource) + + @Provides + @Singleton + fun provideFirebaseUserDataSource(db: FirebaseFirestore): FirebaseUserDataSource = + FirebaseUserDataSourceImpl(db) +} diff --git a/domain/applemusic/.gitignore b/domain/applemusic/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/applemusic/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/applemusic/build.gradle.kts b/domain/applemusic/build.gradle.kts new file mode 100644 index 00000000..566789f1 --- /dev/null +++ b/domain/applemusic/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.musicroad.android.library) +} + +android { + namespace = "com.squirtles.domain.applemusic" +} + +dependencies { + implementation(projects.core.model) + implementation(libs.androidx.paging.runtime) + implementation(libs.inject) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) +} diff --git a/domain/applemusic/proguard-rules.pro b/domain/applemusic/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/domain/applemusic/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/src/main/java/com/squirtles/domain/applemusic/AppleMusicException.kt b/domain/applemusic/src/main/java/com/squirtles/domain/applemusic/AppleMusicException.kt similarity index 100% rename from domain/src/main/java/com/squirtles/domain/applemusic/AppleMusicException.kt rename to domain/applemusic/src/main/java/com/squirtles/domain/applemusic/AppleMusicException.kt diff --git a/domain/src/main/java/com/squirtles/domain/applemusic/AppleMusicRepository.kt b/domain/applemusic/src/main/java/com/squirtles/domain/applemusic/AppleMusicRepository.kt similarity index 66% rename from domain/src/main/java/com/squirtles/domain/applemusic/AppleMusicRepository.kt rename to domain/applemusic/src/main/java/com/squirtles/domain/applemusic/AppleMusicRepository.kt index 4df96e51..485cc364 100644 --- a/domain/src/main/java/com/squirtles/domain/applemusic/AppleMusicRepository.kt +++ b/domain/applemusic/src/main/java/com/squirtles/domain/applemusic/AppleMusicRepository.kt @@ -1,12 +1,11 @@ package com.squirtles.domain.applemusic import androidx.paging.PagingData -import com.squirtles.domain.model.MusicVideo -import com.squirtles.domain.model.Song +import com.squirtles.core.model.MusicVideo +import com.squirtles.core.model.Song import kotlinx.coroutines.flow.Flow interface AppleMusicRepository { fun searchSongs(searchText: String): Flow> - suspend fun searchSongById(songId: String): Result suspend fun searchMusicVideos(searchText: String): Result> } diff --git a/domain/src/main/java/com/squirtles/domain/usecase/music/FetchMusicVideoUseCase.kt b/domain/applemusic/src/main/java/com/squirtles/domain/applemusic/usecase/FetchMusicVideoUseCase.kt similarity index 81% rename from domain/src/main/java/com/squirtles/domain/usecase/music/FetchMusicVideoUseCase.kt rename to domain/applemusic/src/main/java/com/squirtles/domain/applemusic/usecase/FetchMusicVideoUseCase.kt index 1f42c35b..3023cdda 100644 --- a/domain/src/main/java/com/squirtles/domain/usecase/music/FetchMusicVideoUseCase.kt +++ b/domain/applemusic/src/main/java/com/squirtles/domain/applemusic/usecase/FetchMusicVideoUseCase.kt @@ -1,8 +1,8 @@ -package com.squirtles.domain.usecase.music +package com.squirtles.domain.applemusic.usecase -import com.squirtles.domain.model.MusicVideo -import com.squirtles.domain.model.Song import com.squirtles.domain.applemusic.AppleMusicRepository +import com.squirtles.core.model.MusicVideo +import com.squirtles.core.model.Song import javax.inject.Inject class FetchMusicVideoUseCase @Inject constructor( diff --git a/domain/src/main/java/com/squirtles/domain/usecase/music/FetchSongsUseCase.kt b/domain/applemusic/src/main/java/com/squirtles/domain/applemusic/usecase/FetchSongsUseCase.kt similarity index 85% rename from domain/src/main/java/com/squirtles/domain/usecase/music/FetchSongsUseCase.kt rename to domain/applemusic/src/main/java/com/squirtles/domain/applemusic/usecase/FetchSongsUseCase.kt index 54370883..3f7e87fa 100644 --- a/domain/src/main/java/com/squirtles/domain/usecase/music/FetchSongsUseCase.kt +++ b/domain/applemusic/src/main/java/com/squirtles/domain/applemusic/usecase/FetchSongsUseCase.kt @@ -1,4 +1,4 @@ -package com.squirtles.domain.usecase.music +package com.squirtles.domain.applemusic.usecase import com.squirtles.domain.applemusic.AppleMusicRepository import javax.inject.Inject diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts deleted file mode 100644 index 4f4aa66e..00000000 --- a/domain/build.gradle.kts +++ /dev/null @@ -1,61 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.jetbrains.kotlin.android) - alias(libs.plugins.ksp) - alias(libs.plugins.hilt) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "com.squirtles.domain" - compileSdk = 34 - - defaultConfig { - minSdk = 26 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" - } -} - -dependencies { - - implementation(project(":mediaservice")) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - - //hilt - implementation(libs.hilt.android) - ksp(libs.hilt.android.compiler) - androidTestImplementation(libs.hilt.android.testing) - - implementation(libs.inject) - implementation(libs.androidx.paging.runtime) - - implementation(libs.androidx.media3.common) - implementation(libs.androidx.media3.session) - - // Firebase - implementation(libs.firebase.auth.ktx) - - // Serialization - implementation(libs.kotlinx.serialization.json) -} diff --git a/domain/consumer-rules.pro b/domain/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/domain/favorite/.gitignore b/domain/favorite/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/favorite/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/favorite/build.gradle.kts b/domain/favorite/build.gradle.kts new file mode 100644 index 00000000..963e844f --- /dev/null +++ b/domain/favorite/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id(libs.plugins.musicroad.java.library.get().pluginId) +} + +dependencies { + implementation(projects.core.model) + implementation(projects.domain.picklist) + + testImplementation(libs.junit) +} diff --git a/domain/favorite/proguard-rules.pro b/domain/favorite/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/domain/favorite/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/favorite/src/main/java/com/squirtles/domain/favorite/FirebaseFavoriteRepository.kt b/domain/favorite/src/main/java/com/squirtles/domain/favorite/FirebaseFavoriteRepository.kt new file mode 100644 index 00000000..b83b4b8a --- /dev/null +++ b/domain/favorite/src/main/java/com/squirtles/domain/favorite/FirebaseFavoriteRepository.kt @@ -0,0 +1,7 @@ +package com.squirtles.domain.favorite + +interface FirebaseFavoriteRepository { + suspend fun fetchIsFavorite(pickId: String, uid: String): Result + suspend fun createFavorite(pickId: String, uid: String): Result + suspend fun deleteFavorite(pickId: String, uid: String): Result +} diff --git a/domain/favorite/src/main/java/com/squirtles/domain/favorite/usecase/CreateFavoriteUseCase.kt b/domain/favorite/src/main/java/com/squirtles/domain/favorite/usecase/CreateFavoriteUseCase.kt new file mode 100644 index 00000000..b31deb39 --- /dev/null +++ b/domain/favorite/src/main/java/com/squirtles/domain/favorite/usecase/CreateFavoriteUseCase.kt @@ -0,0 +1,11 @@ +package com.squirtles.domain.favorite.usecase + +import com.squirtles.domain.favorite.FirebaseFavoriteRepository +import javax.inject.Inject + +class CreateFavoriteUseCase @Inject constructor( + private val favoriteRepository: FirebaseFavoriteRepository +) { + suspend operator fun invoke(pickId: String, uid: String) = + favoriteRepository.createFavorite(pickId, uid) +} diff --git a/domain/favorite/src/main/java/com/squirtles/domain/favorite/usecase/DeleteFavoriteUseCase.kt b/domain/favorite/src/main/java/com/squirtles/domain/favorite/usecase/DeleteFavoriteUseCase.kt new file mode 100644 index 00000000..358450ab --- /dev/null +++ b/domain/favorite/src/main/java/com/squirtles/domain/favorite/usecase/DeleteFavoriteUseCase.kt @@ -0,0 +1,12 @@ +package com.squirtles.domain.favorite.usecase + +import com.squirtles.domain.favorite.FirebaseFavoriteRepository +import com.squirtles.domain.picklist.RemovePickUseCaseInterface +import javax.inject.Inject + +class DeleteFavoriteUseCase @Inject constructor( + private val favoriteRepository: FirebaseFavoriteRepository +) : RemovePickUseCaseInterface { + override suspend operator fun invoke(pickId: String, uid: String): Result = + favoriteRepository.deleteFavorite(pickId, uid) +} diff --git a/domain/favorite/src/main/java/com/squirtles/domain/favorite/usecase/FetchIsFavoriteUseCase.kt b/domain/favorite/src/main/java/com/squirtles/domain/favorite/usecase/FetchIsFavoriteUseCase.kt new file mode 100644 index 00000000..9a30d0cc --- /dev/null +++ b/domain/favorite/src/main/java/com/squirtles/domain/favorite/usecase/FetchIsFavoriteUseCase.kt @@ -0,0 +1,11 @@ +package com.squirtles.domain.favorite.usecase + +import com.squirtles.domain.favorite.FirebaseFavoriteRepository +import javax.inject.Inject + +class FetchIsFavoriteUseCase @Inject constructor( + private val favoriteRepository: FirebaseFavoriteRepository +) { + suspend operator fun invoke(pickId: String, userId: String) = + favoriteRepository.fetchIsFavorite(pickId, userId) +} diff --git a/domain/firebase/.gitignore b/domain/firebase/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/firebase/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/firebase/build.gradle.kts b/domain/firebase/build.gradle.kts new file mode 100644 index 00000000..4d05a18c --- /dev/null +++ b/domain/firebase/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id(libs.plugins.musicroad.java.library.get().pluginId) +} + +dependencies { + implementation(projects.core.model) + + testImplementation(libs.junit) +} diff --git a/domain/firebase/src/main/java/com/squirtles/domain/firebase/FirebaseException.kt b/domain/firebase/src/main/java/com/squirtles/domain/firebase/FirebaseException.kt new file mode 100644 index 00000000..28243dcf --- /dev/null +++ b/domain/firebase/src/main/java/com/squirtles/domain/firebase/FirebaseException.kt @@ -0,0 +1,49 @@ +package com.squirtles.domain.firebase + +sealed class FirebaseException(override val message: String) : Exception() { + data class CreatedUserFailedException(override val message: String = "Failed to create user") : FirebaseException(message) + data class FetchUserFailedException(override val message: String = "Failed to fetch user") : FirebaseException(message) + data class UpdateUserFailedException(override val message: String = "Failed to update user info") : FirebaseException(message) + data class NoSuchPickException(override val message: String = "No such pick", val pickId: String) + : FirebaseException("message @$pickId") + data class NoSuchPickInRadiusException(override val message: String = "No such pick in area") : FirebaseException(message) + + data class NoSuchDocumentException( + override val message: String = "No such document", + val docId: String, + val collection: String = "" + ) : FirebaseException("$message @$docId in $collection") + + data class FetchDocumentFailedException( + override val message: String = "Failed to fetch document", + val collection: String = "" + ) : FirebaseException("$message in $collection") + + data class AddDocumentFailedException( + override val message: String = "Failed to add document", + val value: Any, + val collection: String = "" + ) : FirebaseException("$message $value in $collection") + + data class DeleteDocumentFailedException( + override val message: String = "Failed to delete document", + val docId: String, + val collection: String = "" + ): FirebaseException("$message @$docId in $collection") + + data class UpdateDocumentFailedException( + override val message: String = "Failed to update document", + val docId: String, + val collection: String = "" + ) : FirebaseException("$message @$docId in $collection") + + data class ExecuteQueryFailedException( + override val message: String = "Failed to execute query", + val collection: String = "" + ) : FirebaseException("$message in $collection") + + data class CloudFunctionFailedException( + override val message: String = "Failed to run cloud function", + val exceptionMessage: String + ) : FirebaseException("$message : $exceptionMessage") +} diff --git a/domain/location/.gitignore b/domain/location/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/location/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/location/build.gradle.kts b/domain/location/build.gradle.kts new file mode 100644 index 00000000..5bbf924f --- /dev/null +++ b/domain/location/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.musicroad.android.library) +} + +android { + namespace = "com.squirtles.domain.location" +} + +dependencies { + implementation(libs.inject) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) +} diff --git a/domain/location/proguard-rules.pro b/domain/location/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/domain/location/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/location/src/main/java/com/squirtles/domain/location/LocalLocationRepository.kt b/domain/location/src/main/java/com/squirtles/domain/location/LocalLocationRepository.kt new file mode 100644 index 00000000..943b5b00 --- /dev/null +++ b/domain/location/src/main/java/com/squirtles/domain/location/LocalLocationRepository.kt @@ -0,0 +1,10 @@ +package com.squirtles.domain.location + +import android.location.Location +import kotlinx.coroutines.flow.StateFlow + +interface LocalLocationRepository { + val lastLocation: StateFlow + + suspend fun saveCurrentLocation(geoLocation: Location) +} diff --git a/domain/location/src/main/java/com/squirtles/domain/location/usecase/GetLastLocationUseCase.kt b/domain/location/src/main/java/com/squirtles/domain/location/usecase/GetLastLocationUseCase.kt new file mode 100644 index 00000000..d4a29cfc --- /dev/null +++ b/domain/location/src/main/java/com/squirtles/domain/location/usecase/GetLastLocationUseCase.kt @@ -0,0 +1,10 @@ +package com.squirtles.domain.location.usecase + +import com.squirtles.domain.location.LocalLocationRepository +import javax.inject.Inject + +class GetLastLocationUseCase @Inject constructor( + private val localLocationRepository: LocalLocationRepository +) { + operator fun invoke() = localLocationRepository.lastLocation +} diff --git a/domain/location/src/main/java/com/squirtles/domain/location/usecase/SaveLastLocationUseCase.kt b/domain/location/src/main/java/com/squirtles/domain/location/usecase/SaveLastLocationUseCase.kt new file mode 100644 index 00000000..62e0226b --- /dev/null +++ b/domain/location/src/main/java/com/squirtles/domain/location/usecase/SaveLastLocationUseCase.kt @@ -0,0 +1,11 @@ +package com.squirtles.domain.location.usecase + +import android.location.Location +import com.squirtles.domain.location.LocalLocationRepository +import javax.inject.Inject + +class SaveLastLocationUseCase @Inject constructor( + private val localLocationRepository: LocalLocationRepository +) { + suspend operator fun invoke(location: Location) = localLocationRepository.saveCurrentLocation(location) +} diff --git a/domain/order/.gitignore b/domain/order/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/order/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/order/build.gradle.kts b/domain/order/build.gradle.kts new file mode 100644 index 00000000..d45c0a97 --- /dev/null +++ b/domain/order/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id(libs.plugins.musicroad.java.library.get().pluginId) +} + +dependencies { + implementation(projects.core.model) + implementation(projects.domain.picklist) +} diff --git a/domain/order/proguard-rules.pro b/domain/order/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/domain/order/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/order/src/main/java/com/squirtles/domain/order/LocalPickListOrderRepository.kt b/domain/order/src/main/java/com/squirtles/domain/order/LocalPickListOrderRepository.kt new file mode 100644 index 00000000..04c3bfe5 --- /dev/null +++ b/domain/order/src/main/java/com/squirtles/domain/order/LocalPickListOrderRepository.kt @@ -0,0 +1,11 @@ +package com.squirtles.domain.order + +import com.squirtles.core.model.Order + +interface LocalPickListOrderRepository { + val favoriteListOrder: Order // 픽 보관함 정렬 순서 + val myListOrder: Order // 등록한 픽 정렬 순서 + + suspend fun saveFavoriteListOrder(order: Order) + suspend fun saveMyListOrder(order: Order) +} diff --git a/domain/order/src/main/java/com/squirtles/domain/order/usecase/GetFavoriteListOrderUseCase.kt b/domain/order/src/main/java/com/squirtles/domain/order/usecase/GetFavoriteListOrderUseCase.kt new file mode 100644 index 00000000..b7d9b0db --- /dev/null +++ b/domain/order/src/main/java/com/squirtles/domain/order/usecase/GetFavoriteListOrderUseCase.kt @@ -0,0 +1,11 @@ +package com.squirtles.domain.order.usecase + +import com.squirtles.domain.order.LocalPickListOrderRepository +import com.squirtles.domain.picklist.GetPickListOrderUseCaseInterface +import javax.inject.Inject + +class GetFavoriteListOrderUseCase @Inject constructor( + private val localPickListOrderRepository: LocalPickListOrderRepository +) : GetPickListOrderUseCaseInterface { + override suspend operator fun invoke() = localPickListOrderRepository.favoriteListOrder +} diff --git a/domain/order/src/main/java/com/squirtles/domain/order/usecase/GetMyPickListOrderUseCase.kt b/domain/order/src/main/java/com/squirtles/domain/order/usecase/GetMyPickListOrderUseCase.kt new file mode 100644 index 00000000..ab8b72e6 --- /dev/null +++ b/domain/order/src/main/java/com/squirtles/domain/order/usecase/GetMyPickListOrderUseCase.kt @@ -0,0 +1,11 @@ +package com.squirtles.domain.order.usecase + +import com.squirtles.domain.order.LocalPickListOrderRepository +import com.squirtles.domain.picklist.GetPickListOrderUseCaseInterface +import javax.inject.Inject + +class GetMyPickListOrderUseCase @Inject constructor( + private val localPickListOrderRepository: LocalPickListOrderRepository +) : GetPickListOrderUseCaseInterface { + override suspend operator fun invoke() = localPickListOrderRepository.myListOrder +} diff --git a/domain/order/src/main/java/com/squirtles/domain/order/usecase/SaveFavoriteListOrderUseCase.kt b/domain/order/src/main/java/com/squirtles/domain/order/usecase/SaveFavoriteListOrderUseCase.kt new file mode 100644 index 00000000..4115a7c0 --- /dev/null +++ b/domain/order/src/main/java/com/squirtles/domain/order/usecase/SaveFavoriteListOrderUseCase.kt @@ -0,0 +1,12 @@ +package com.squirtles.domain.order.usecase + +import com.squirtles.core.model.Order +import com.squirtles.domain.order.LocalPickListOrderRepository +import com.squirtles.domain.picklist.SavePickListOrderUseCaseInterface +import javax.inject.Inject + +class SaveFavoriteListOrderUseCase @Inject constructor( + private val localPickListOrderRepository: LocalPickListOrderRepository +) : SavePickListOrderUseCaseInterface { + override suspend operator fun invoke(order: Order) = localPickListOrderRepository.saveFavoriteListOrder(order) +} diff --git a/domain/order/src/main/java/com/squirtles/domain/order/usecase/SaveMyPickListOrderUseCase.kt b/domain/order/src/main/java/com/squirtles/domain/order/usecase/SaveMyPickListOrderUseCase.kt new file mode 100644 index 00000000..2aea91f0 --- /dev/null +++ b/domain/order/src/main/java/com/squirtles/domain/order/usecase/SaveMyPickListOrderUseCase.kt @@ -0,0 +1,12 @@ +package com.squirtles.domain.order.usecase + +import com.squirtles.core.model.Order +import com.squirtles.domain.order.LocalPickListOrderRepository +import com.squirtles.domain.picklist.SavePickListOrderUseCaseInterface +import javax.inject.Inject + +class SaveMyPickListOrderUseCase @Inject constructor( + private val localPickListOrderRepository: LocalPickListOrderRepository +) : SavePickListOrderUseCaseInterface { + override suspend operator fun invoke(order: Order) = localPickListOrderRepository.saveMyListOrder(order) +} diff --git a/domain/pick/.gitignore b/domain/pick/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/pick/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/pick/build.gradle.kts b/domain/pick/build.gradle.kts new file mode 100644 index 00000000..6b0a474a --- /dev/null +++ b/domain/pick/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id(libs.plugins.musicroad.java.library.get().pluginId) +} + +dependencies { + implementation(projects.core.model) + implementation(projects.domain.picklist) + + implementation(libs.kotlinx.coroutines.core) +} diff --git a/domain/pick/proguard-rules.pro b/domain/pick/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/domain/pick/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/pick/src/main/java/com/squirtles/domain/pick/FirebasePickRepository.kt b/domain/pick/src/main/java/com/squirtles/domain/pick/FirebasePickRepository.kt new file mode 100644 index 00000000..c022f98a --- /dev/null +++ b/domain/pick/src/main/java/com/squirtles/domain/pick/FirebasePickRepository.kt @@ -0,0 +1,13 @@ +package com.squirtles.domain.pick + +import com.squirtles.core.model.Pick +import kotlinx.coroutines.flow.Flow + +interface FirebasePickRepository { + suspend fun createPick(pick: Pick): Result + suspend fun deletePick(pickId: String, userId: String): Result + suspend fun fetchPick(pickId: String): Result + suspend fun fetchPicksInArea(lat: Double, lng: Double, radiusInM: Double): Flow> + suspend fun fetchMyPicks(userId: String): Result> + suspend fun fetchFavoritePicks(userId: String): Result> +} diff --git a/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/CreatePickUseCase.kt b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/CreatePickUseCase.kt new file mode 100644 index 00000000..48df71441 --- /dev/null +++ b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/CreatePickUseCase.kt @@ -0,0 +1,11 @@ +package com.squirtles.domain.pick.usecase + +import com.squirtles.core.model.Pick +import com.squirtles.domain.pick.FirebasePickRepository +import javax.inject.Inject + +class CreatePickUseCase @Inject constructor( + private val pickRepository: FirebasePickRepository +) { + suspend operator fun invoke(pick: Pick): Result = pickRepository.createPick(pick) +} diff --git a/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/DeletePickUseCase.kt b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/DeletePickUseCase.kt new file mode 100644 index 00000000..6c553669 --- /dev/null +++ b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/DeletePickUseCase.kt @@ -0,0 +1,12 @@ +package com.squirtles.domain.pick.usecase + +import com.squirtles.domain.pick.FirebasePickRepository +import com.squirtles.domain.picklist.RemovePickUseCaseInterface +import javax.inject.Inject + +class DeletePickUseCase @Inject constructor( + private val pickRepository: FirebasePickRepository +) : RemovePickUseCaseInterface { + override suspend operator fun invoke(pickId: String, uid: String): Result = + pickRepository.deletePick(pickId, uid) +} diff --git a/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/FetchFavoritePicksUseCase.kt b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/FetchFavoritePicksUseCase.kt new file mode 100644 index 00000000..1eb8c16b --- /dev/null +++ b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/FetchFavoritePicksUseCase.kt @@ -0,0 +1,13 @@ +package com.squirtles.domain.pick.usecase + +import com.squirtles.core.model.Pick +import com.squirtles.domain.pick.FirebasePickRepository +import com.squirtles.domain.picklist.FetchPickListUseCaseInterface +import javax.inject.Inject + +class FetchFavoritePicksUseCase @Inject constructor( + private val pickRepository: FirebasePickRepository +) : FetchPickListUseCaseInterface { + override suspend operator fun invoke(userId: String): Result> = + pickRepository.fetchFavoritePicks(userId) +} diff --git a/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/FetchMyPicksUseCase.kt b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/FetchMyPicksUseCase.kt new file mode 100644 index 00000000..cd410c9f --- /dev/null +++ b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/FetchMyPicksUseCase.kt @@ -0,0 +1,13 @@ +package com.squirtles.domain.pick.usecase + +import com.squirtles.core.model.Pick +import com.squirtles.domain.pick.FirebasePickRepository +import com.squirtles.domain.picklist.FetchPickListUseCaseInterface +import javax.inject.Inject + +class FetchMyPicksUseCase @Inject constructor( + private val pickRepository: FirebasePickRepository +) : FetchPickListUseCaseInterface { + override suspend operator fun invoke(userId: String) : Result> = + pickRepository.fetchMyPicks(userId) +} diff --git a/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/FetchPickUseCase.kt b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/FetchPickUseCase.kt new file mode 100644 index 00000000..d4dee2f4 --- /dev/null +++ b/domain/pick/src/main/java/com/squirtles/domain/pick/usecase/FetchPickUseCase.kt @@ -0,0 +1,16 @@ +package com.squirtles.domain.pick.usecase + +import com.squirtles.core.model.Pick +import com.squirtles.domain.pick.FirebasePickRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class FetchPickUseCase @Inject constructor( + private val pickRepository: FirebasePickRepository +) { + suspend operator fun invoke(pickId: String): Result = + pickRepository.fetchPick(pickId) + + suspend operator fun invoke(lat: Double, lng: Double, radiusInM: Double): Flow> = + pickRepository.fetchPicksInArea(lat, lng, radiusInM) +} diff --git a/domain/picklist/.gitignore b/domain/picklist/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/picklist/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/picklist/build.gradle.kts b/domain/picklist/build.gradle.kts new file mode 100644 index 00000000..e0a1f12a --- /dev/null +++ b/domain/picklist/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id(libs.plugins.musicroad.java.library.get().pluginId) +} + +dependencies { + implementation(projects.core.model) +} diff --git a/domain/picklist/proguard-rules.pro b/domain/picklist/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/domain/picklist/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/picklist/src/main/java/com/squirtles/domain/picklist/FetchPickListUseCaseInterface.kt b/domain/picklist/src/main/java/com/squirtles/domain/picklist/FetchPickListUseCaseInterface.kt new file mode 100644 index 00000000..bfa4beae --- /dev/null +++ b/domain/picklist/src/main/java/com/squirtles/domain/picklist/FetchPickListUseCaseInterface.kt @@ -0,0 +1,7 @@ +package com.squirtles.domain.picklist + +import com.squirtles.core.model.Pick + +interface FetchPickListUseCaseInterface { + suspend operator fun invoke(userId: String): Result> +} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/picklist/GetPickListOrderUseCaseInterface.kt b/domain/picklist/src/main/java/com/squirtles/domain/picklist/GetPickListOrderUseCaseInterface.kt similarity index 51% rename from domain/src/main/java/com/squirtles/domain/usecase/picklist/GetPickListOrderUseCaseInterface.kt rename to domain/picklist/src/main/java/com/squirtles/domain/picklist/GetPickListOrderUseCaseInterface.kt index ad98b854..40833983 100644 --- a/domain/src/main/java/com/squirtles/domain/usecase/picklist/GetPickListOrderUseCaseInterface.kt +++ b/domain/picklist/src/main/java/com/squirtles/domain/picklist/GetPickListOrderUseCaseInterface.kt @@ -1,6 +1,6 @@ -package com.squirtles.domain.usecase.picklist +package com.squirtles.domain.picklist -import com.squirtles.domain.model.Order +import com.squirtles.core.model.Order interface GetPickListOrderUseCaseInterface { suspend operator fun invoke(): Order diff --git a/domain/picklist/src/main/java/com/squirtles/domain/picklist/RemovePickUseCaseInterface.kt b/domain/picklist/src/main/java/com/squirtles/domain/picklist/RemovePickUseCaseInterface.kt new file mode 100644 index 00000000..fc6cbc06 --- /dev/null +++ b/domain/picklist/src/main/java/com/squirtles/domain/picklist/RemovePickUseCaseInterface.kt @@ -0,0 +1,5 @@ +package com.squirtles.domain.picklist + +interface RemovePickUseCaseInterface { + suspend operator fun invoke(pickId: String, uid: String): Result +} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/picklist/SavePickListOrderUseCaseInterface.kt b/domain/picklist/src/main/java/com/squirtles/domain/picklist/SavePickListOrderUseCaseInterface.kt similarity index 52% rename from domain/src/main/java/com/squirtles/domain/usecase/picklist/SavePickListOrderUseCaseInterface.kt rename to domain/picklist/src/main/java/com/squirtles/domain/picklist/SavePickListOrderUseCaseInterface.kt index eb04b50e..350aebf0 100644 --- a/domain/src/main/java/com/squirtles/domain/usecase/picklist/SavePickListOrderUseCaseInterface.kt +++ b/domain/picklist/src/main/java/com/squirtles/domain/picklist/SavePickListOrderUseCaseInterface.kt @@ -1,6 +1,6 @@ -package com.squirtles.domain.usecase.picklist +package com.squirtles.domain.picklist -import com.squirtles.domain.model.Order +import com.squirtles.core.model.Order interface SavePickListOrderUseCaseInterface { suspend operator fun invoke(order: Order) diff --git a/domain/player/.gitignore b/domain/player/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/player/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/player/build.gradle.kts b/domain/player/build.gradle.kts new file mode 100644 index 00000000..88dea554 --- /dev/null +++ b/domain/player/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.musicroad.android.library) +} + +android { + namespace = "com.squirtles.domain.player" +} + +dependencies { + implementation(projects.core.model) + implementation(projects.core.mediaservice) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + implementation(libs.inject) + implementation(libs.bundles.media3) +} diff --git a/domain/player/proguard-rules.pro b/domain/player/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/domain/player/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/src/main/java/com/squirtles/domain/usecase/player/MediaPlayerListenerUseCase.kt b/domain/player/src/main/java/com/squirtles/domain/player/MediaPlayerListenerUseCase.kt similarity index 97% rename from domain/src/main/java/com/squirtles/domain/usecase/player/MediaPlayerListenerUseCase.kt rename to domain/player/src/main/java/com/squirtles/domain/player/MediaPlayerListenerUseCase.kt index 82f57e80..380020ab 100644 --- a/domain/src/main/java/com/squirtles/domain/usecase/player/MediaPlayerListenerUseCase.kt +++ b/domain/player/src/main/java/com/squirtles/domain/player/MediaPlayerListenerUseCase.kt @@ -1,9 +1,9 @@ -package com.squirtles.domain.usecase.player +package com.squirtles.domain.player import androidx.media3.common.Player import androidx.media3.common.Tracks -import com.squirtles.domain.model.PlayerState -import com.squirtles.mediaservice.MediaControllerProvider +import com.squirtles.core.mediaservice.MediaControllerProvider +import com.squirtles.core.model.PlayerState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job diff --git a/domain/src/main/java/com/squirtles/domain/usecase/player/MediaPlayerUseCase.kt b/domain/player/src/main/java/com/squirtles/domain/player/MediaPlayerUseCase.kt similarity index 91% rename from domain/src/main/java/com/squirtles/domain/usecase/player/MediaPlayerUseCase.kt rename to domain/player/src/main/java/com/squirtles/domain/player/MediaPlayerUseCase.kt index 218e6388..74fe53dd 100644 --- a/domain/src/main/java/com/squirtles/domain/usecase/player/MediaPlayerUseCase.kt +++ b/domain/player/src/main/java/com/squirtles/domain/player/MediaPlayerUseCase.kt @@ -1,11 +1,12 @@ -package com.squirtles.domain.usecase.player +package com.squirtles.domain.player import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.Player import androidx.media3.session.MediaController -import com.squirtles.domain.model.Pick -import com.squirtles.mediaservice.MediaControllerProvider +import com.squirtles.core.mediaservice.MediaControllerProvider +import com.squirtles.core.mediaservice.SEEK_TO_DURATION +import com.squirtles.core.model.Pick import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import javax.inject.Inject @@ -98,13 +99,13 @@ class MediaPlayerUseCase @Inject constructor( fun advanceBy() { mediaController?.apply { - seekTo(currentPosition + com.squirtles.mediaservice.SEEK_TO_DURATION) + seekTo(currentPosition + SEEK_TO_DURATION) } } fun rewindBy() { mediaController?.apply { - seekTo(currentPosition - com.squirtles.mediaservice.SEEK_TO_DURATION) + seekTo(currentPosition - SEEK_TO_DURATION) } } diff --git a/domain/src/main/AndroidManifest.xml b/domain/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68..00000000 --- a/domain/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/domain/src/main/java/com/squirtles/domain/applemusic/AppleMusicRemoteDataSource.kt b/domain/src/main/java/com/squirtles/domain/applemusic/AppleMusicRemoteDataSource.kt deleted file mode 100644 index 752dd499..00000000 --- a/domain/src/main/java/com/squirtles/domain/applemusic/AppleMusicRemoteDataSource.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.squirtles.domain.applemusic - -import androidx.paging.PagingData -import com.squirtles.domain.model.MusicVideo -import com.squirtles.domain.model.Song -import kotlinx.coroutines.flow.Flow - -interface AppleMusicRemoteDataSource { - fun searchSongs(searchText: String): Flow> - suspend fun searchSongById(songId: String): Song - suspend fun searchMusicVideos(searchText: String): List -} diff --git a/domain/src/main/java/com/squirtles/domain/firebase/FirebaseException.kt b/domain/src/main/java/com/squirtles/domain/firebase/FirebaseException.kt deleted file mode 100644 index 946d7310..00000000 --- a/domain/src/main/java/com/squirtles/domain/firebase/FirebaseException.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.squirtles.domain.firebase - -sealed class FirebaseException(override val message: String) : Exception() { - data class CreatedUserFailedException(override val message: String = "Failed to create a user") : FirebaseException(message) - data class UserNotFoundException(override val message: String = "Failed to fetch a user") : FirebaseException(message) - data class NoSuchPickException(override val message: String = "No such pick") : FirebaseException(message) - data class NoSuchPickInRadiusException(override val message: String = "No such pick in area") : FirebaseException(message) -} diff --git a/domain/src/main/java/com/squirtles/domain/firebase/FirebaseRemoteDataSource.kt b/domain/src/main/java/com/squirtles/domain/firebase/FirebaseRemoteDataSource.kt deleted file mode 100644 index 8c74ac22..00000000 --- a/domain/src/main/java/com/squirtles/domain/firebase/FirebaseRemoteDataSource.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.squirtles.domain.firebase - -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.User -import kotlinx.coroutines.flow.Flow - -data class PickWithType( - val type: PickType, - val pick: Pick -) - -enum class PickType { - UPDATED, REMOVED -} - -interface FirebaseRemoteDataSource { - suspend fun createGoogleIdUser(uid: String, email: String, userName: String?, userProfileImage: String?): User? - suspend fun fetchUser(uid: String): User? - suspend fun updateUserName(uid: String, newUserName: String): Boolean - suspend fun deleteUser(uid: String): Boolean - - suspend fun fetchPick(pickID: String): Pick? - suspend fun fetchPicksInArea(lat: Double, lng: Double, radiusInM: Double): Flow> - suspend fun createPick(pick: Pick): String - suspend fun deletePick(pickId: String, uid: String): Boolean - - suspend fun fetchMyPicks(uid: String): List - suspend fun fetchFavoritePicks(uid: String): List - suspend fun fetchIsFavorite(pickId: String, uid: String): Boolean - suspend fun createFavorite(pickId: String, uid: String): Boolean - suspend fun deleteFavorite(pickId: String, uid: String): Boolean -// suspend fun updatePick(pick: Pick) -} diff --git a/domain/src/main/java/com/squirtles/domain/firebase/FirebaseRepository.kt b/domain/src/main/java/com/squirtles/domain/firebase/FirebaseRepository.kt deleted file mode 100644 index 77cf04ae..00000000 --- a/domain/src/main/java/com/squirtles/domain/firebase/FirebaseRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.squirtles.domain.firebase - -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.User -import kotlinx.coroutines.flow.Flow - -interface FirebaseRepository { - suspend fun createGoogleIdUser(uid: String, email: String, userName: String?, userProfileImage: String?): Result - suspend fun fetchUser(uid: String): Result - suspend fun updateUserName(uid: String, newUserName: String): Result - suspend fun deleteUser(uid: String): Result - - suspend fun fetchPick(pickID: String): Result - suspend fun fetchPicksInArea(lat: Double, lng: Double, radiusInM: Double): Flow> - suspend fun createPick(pick: Pick): Result - suspend fun deletePick(pickId: String, uid: String): Result - - suspend fun fetchMyPicks(uid: String): Result> - suspend fun fetchFavoritePicks(uid: String): Result> - suspend fun fetchIsFavorite(pickId: String, uid: String): Result - suspend fun createFavorite(pickId: String, uid: String): Result - suspend fun deleteFavorite(pickId: String, uid: String): Result -} diff --git a/domain/src/main/java/com/squirtles/domain/local/LocalDataSource.kt b/domain/src/main/java/com/squirtles/domain/local/LocalDataSource.kt deleted file mode 100644 index 3d6bce79..00000000 --- a/domain/src/main/java/com/squirtles/domain/local/LocalDataSource.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.squirtles.domain.local - -import android.location.Location -import com.squirtles.domain.model.Order -import com.squirtles.domain.model.User -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow - -interface LocalDataSource { - val currentUser: User? - val lastLocation: StateFlow - val favoriteListOrder: Order - val myListOrder: Order - - fun readUidDataStore(): Flow - suspend fun saveUidDataStore(uid: String) - suspend fun saveCurrentUser(user: User) - suspend fun clearUser() - suspend fun saveCurrentLocation(location: Location) - suspend fun saveFavoriteListOrder(order: Order) - suspend fun saveMyListOrder(order: Order) -} diff --git a/domain/src/main/java/com/squirtles/domain/local/LocalRepository.kt b/domain/src/main/java/com/squirtles/domain/local/LocalRepository.kt deleted file mode 100644 index 9fcb7542..00000000 --- a/domain/src/main/java/com/squirtles/domain/local/LocalRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.squirtles.domain.local - -import android.location.Location -import com.squirtles.domain.model.Order -import com.squirtles.domain.model.User -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow - -interface LocalRepository { - val lastLocation: StateFlow - val favoriteListOrder: Order // 픽 보관함 정렬 순서 - val myListOrder: Order // 등록한 픽 정렬 순서 - - fun readUidDataStore(): Flow - suspend fun clearUser(): Result - suspend fun saveCurrentLocation(location: Location) - suspend fun saveFavoriteListOrder(order: Order) - suspend fun saveMyListOrder(order: Order) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/favorite/CreateFavoriteUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/favorite/CreateFavoriteUseCase.kt deleted file mode 100644 index 90ddaf45..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/favorite/CreateFavoriteUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.favorite - -import com.squirtles.domain.firebase.FirebaseRepository -import javax.inject.Inject - -class CreateFavoriteUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) { - suspend operator fun invoke(pickId: String, uid: String) = - firebaseRepository.createFavorite(pickId, uid) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/favorite/DeleteFavoriteUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/favorite/DeleteFavoriteUseCase.kt deleted file mode 100644 index 0dd366a5..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/favorite/DeleteFavoriteUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.squirtles.domain.usecase.favorite - -import com.squirtles.domain.firebase.FirebaseRepository -import com.squirtles.domain.usecase.picklist.DeletePickListUseCaseInterface -import javax.inject.Inject - -class DeleteFavoriteUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) : DeletePickListUseCaseInterface { - override suspend operator fun invoke(pickId: String, uid: String) = - firebaseRepository.deleteFavorite(pickId, uid) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/favorite/FetchFavoritePicksUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/favorite/FetchFavoritePicksUseCase.kt deleted file mode 100644 index 2fa26e28..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/favorite/FetchFavoritePicksUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.favorite - -import com.squirtles.domain.firebase.FirebaseRepository -import com.squirtles.domain.usecase.picklist.FetchPickListUseCaseInterface -import javax.inject.Inject - -class FetchFavoritePicksUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) : FetchPickListUseCaseInterface { - override suspend operator fun invoke(uid: String) = firebaseRepository.fetchFavoritePicks(uid) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/location/GetLastLocationUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/location/GetLastLocationUseCase.kt deleted file mode 100644 index d081a9d9..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/location/GetLastLocationUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.squirtles.domain.usecase.location - -import com.squirtles.domain.local.LocalRepository -import javax.inject.Inject - -class GetLastLocationUseCase @Inject constructor( - private val localRepository: LocalRepository -) { - operator fun invoke() = localRepository.lastLocation -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/location/SaveLastLocationUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/location/SaveLastLocationUseCase.kt deleted file mode 100644 index 8e333af9..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/location/SaveLastLocationUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.location - -import android.location.Location -import com.squirtles.domain.local.LocalRepository -import javax.inject.Inject - -class SaveLastLocationUseCase @Inject constructor( - private val localRepository: LocalRepository -) { - suspend operator fun invoke(location: Location) = localRepository.saveCurrentLocation(location) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/mypick/CreatePickUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/mypick/CreatePickUseCase.kt deleted file mode 100644 index f0816446..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/mypick/CreatePickUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.mypick - -import com.squirtles.domain.model.Pick -import com.squirtles.domain.firebase.FirebaseRepository -import javax.inject.Inject - -class CreatePickUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) { - suspend operator fun invoke(pick: Pick): Result = firebaseRepository.createPick(pick) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/mypick/DeletePickUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/mypick/DeletePickUseCase.kt deleted file mode 100644 index 71a7670a..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/mypick/DeletePickUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.squirtles.domain.usecase.mypick - -import com.squirtles.domain.firebase.FirebaseRepository -import com.squirtles.domain.usecase.picklist.DeletePickListUseCaseInterface -import javax.inject.Inject - -class DeletePickUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) : DeletePickListUseCaseInterface { - override suspend operator fun invoke(pickId: String, uid: String): Result = - firebaseRepository.deletePick(pickId, uid) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/mypick/FetchMyPicksUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/mypick/FetchMyPicksUseCase.kt deleted file mode 100644 index 7daeef0a..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/mypick/FetchMyPicksUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.squirtles.domain.usecase.mypick - -import com.squirtles.domain.firebase.FirebaseRepository -import com.squirtles.domain.usecase.picklist.FetchPickListUseCaseInterface -import javax.inject.Inject - -class FetchMyPicksUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) : FetchPickListUseCaseInterface { - override suspend operator fun invoke(uid: String) = - firebaseRepository.fetchMyPicks(uid) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/order/GetFavoriteListOrderUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/order/GetFavoriteListOrderUseCase.kt deleted file mode 100644 index 98a20b6a..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/order/GetFavoriteListOrderUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.order - -import com.squirtles.domain.local.LocalRepository -import com.squirtles.domain.usecase.picklist.GetPickListOrderUseCaseInterface -import javax.inject.Inject - -class GetFavoriteListOrderUseCase @Inject constructor( - private val localRepository: LocalRepository -) : GetPickListOrderUseCaseInterface { - override suspend operator fun invoke() = localRepository.favoriteListOrder -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/order/GetMyPickListOrderUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/order/GetMyPickListOrderUseCase.kt deleted file mode 100644 index b41e7d66..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/order/GetMyPickListOrderUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.order - -import com.squirtles.domain.local.LocalRepository -import com.squirtles.domain.usecase.picklist.GetPickListOrderUseCaseInterface -import javax.inject.Inject - -class GetMyPickListOrderUseCase @Inject constructor( - private val localRepository: LocalRepository -) : GetPickListOrderUseCaseInterface { - override suspend operator fun invoke() = localRepository.myListOrder -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/order/SaveFavoriteListOrderUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/order/SaveFavoriteListOrderUseCase.kt deleted file mode 100644 index 8b7ed05f..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/order/SaveFavoriteListOrderUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.squirtles.domain.usecase.order - -import com.squirtles.domain.local.LocalRepository -import com.squirtles.domain.model.Order -import com.squirtles.domain.usecase.picklist.SavePickListOrderUseCaseInterface -import javax.inject.Inject - -class SaveFavoriteListOrderUseCase @Inject constructor( - private val localRepository: LocalRepository -) : SavePickListOrderUseCaseInterface { - override suspend operator fun invoke(order: Order) = localRepository.saveFavoriteListOrder(order) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/order/SaveMyPickListOrderUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/order/SaveMyPickListOrderUseCase.kt deleted file mode 100644 index 0ca672d7..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/order/SaveMyPickListOrderUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.squirtles.domain.usecase.order - -import com.squirtles.domain.local.LocalRepository -import com.squirtles.domain.model.Order -import com.squirtles.domain.usecase.picklist.SavePickListOrderUseCaseInterface -import javax.inject.Inject - -class SaveMyPickListOrderUseCase @Inject constructor( - private val localRepository: LocalRepository -) : SavePickListOrderUseCaseInterface { - override suspend operator fun invoke(order: Order) = localRepository.saveMyListOrder(order) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/pick/FetchIsFavoriteUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/pick/FetchIsFavoriteUseCase.kt deleted file mode 100644 index 3b5f141d..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/pick/FetchIsFavoriteUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.pick - -import com.squirtles.domain.firebase.FirebaseRepository -import javax.inject.Inject - -class FetchIsFavoriteUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) { - suspend operator fun invoke(pickId: String, uid: String) = - firebaseRepository.fetchIsFavorite(pickId, uid) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/pick/FetchPickInAreaUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/pick/FetchPickInAreaUseCase.kt deleted file mode 100644 index 053d230e..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/pick/FetchPickInAreaUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.pick - -import com.squirtles.domain.firebase.FirebaseRepository -import javax.inject.Inject - -class FetchPickInAreaUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) { - suspend operator fun invoke(lat: Double, lng: Double, radiusInM: Double) = - firebaseRepository.fetchPicksInArea(lat, lng, radiusInM) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/pick/FetchPickUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/pick/FetchPickUseCase.kt deleted file mode 100644 index 37c7e62b..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/pick/FetchPickUseCase.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.squirtles.domain.usecase.pick - -import com.squirtles.domain.firebase.FirebaseRepository -import javax.inject.Inject - -class FetchPickUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) { - suspend operator fun invoke(pickId: String) = - firebaseRepository.fetchPick(pickId) - - suspend operator fun invoke(lat: Double, lng: Double, radiusInM: Double) = - firebaseRepository.fetchPicksInArea(lat, lng, radiusInM) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/picklist/DeletePickListUseCaseInterface.kt b/domain/src/main/java/com/squirtles/domain/usecase/picklist/DeletePickListUseCaseInterface.kt deleted file mode 100644 index 752c1cba..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/picklist/DeletePickListUseCaseInterface.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.squirtles.domain.usecase.picklist - -interface DeletePickListUseCaseInterface { - suspend operator fun invoke(pickId: String, uid: String): Result -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/picklist/FetchPickListUseCaseInterface.kt b/domain/src/main/java/com/squirtles/domain/usecase/picklist/FetchPickListUseCaseInterface.kt deleted file mode 100644 index fe466d0c..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/picklist/FetchPickListUseCaseInterface.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.squirtles.domain.usecase.picklist - -import com.squirtles.domain.model.Pick - -interface FetchPickListUseCaseInterface { - suspend operator fun invoke(uid: String): Result> -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/user/DeleteAccountUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/user/DeleteAccountUseCase.kt deleted file mode 100644 index 7fc0a7d7..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/user/DeleteAccountUseCase.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.squirtles.domain.usecase.user - -import android.util.Log -import com.google.firebase.auth.FirebaseAuth -import com.squirtles.domain.firebase.FirebaseRepository -import com.squirtles.domain.usecase.favorite.DeleteFavoriteUseCase -import com.squirtles.domain.usecase.favorite.FetchFavoritePicksUseCase -import com.squirtles.domain.usecase.mypick.DeletePickUseCase -import com.squirtles.domain.usecase.mypick.FetchMyPicksUseCase -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import javax.inject.Inject - -class DeleteAccountUseCase @Inject constructor( - private val fetchFavoritePicksUseCase: FetchFavoritePicksUseCase, - private val deleteFavoriteUseCase: DeleteFavoriteUseCase, - private val fetchMyPicksUseCase: FetchMyPicksUseCase, - private val deletePickUseCase: DeletePickUseCase, - private val firebaseRepository: FirebaseRepository -) { - suspend operator fun invoke() = coroutineScope { - FirebaseAuth.getInstance().currentUser?.let { currentUser -> - try { - // 1. 좋아한 픽 삭제 - val favoritePicks = fetchFavoritePicksUseCase(currentUser.uid).getOrNull() ?: emptyList() - val favoritePicksDeleteJobs = favoritePicks.map { - async { deleteFavoriteUseCase(it.id, currentUser.uid) } - } - - // 2. 등록한 픽 삭제 - val myPicks = fetchMyPicksUseCase(currentUser.uid).getOrNull() ?: emptyList() - val myPicksDeleteJobs = myPicks.map { - async { deletePickUseCase(it.id, currentUser.uid) } - } - - // 모든 삭제 작업이 끝날 때까지 기다림 - (favoritePicksDeleteJobs + myPicksDeleteJobs).awaitAll() - - // 3. Firebase Firestore 유저 정보 삭제 - firebaseRepository.deleteUser(currentUser.uid) - - // 4. Firebase Auth 유저 삭제 - currentUser.delete() - } catch (e: Exception) { - Log.e("DeleteAccount", "Error deleting user account: ${e.message}") - } - } - } -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/user/FetchUserByIdUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/user/FetchUserByIdUseCase.kt deleted file mode 100644 index 780d551b..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/user/FetchUserByIdUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.user - -import com.squirtles.domain.firebase.FirebaseRepository -import javax.inject.Inject - -class FetchUserByIdUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) { - suspend operator fun invoke(userId: String) = - firebaseRepository.fetchUser(userId) -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/user/GetCurrentUidUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/user/GetCurrentUidUseCase.kt deleted file mode 100644 index 6837f153..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/user/GetCurrentUidUseCase.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.squirtles.domain.usecase.user - -import com.google.firebase.auth.FirebaseAuth -import javax.inject.Inject - -class GetCurrentUidUseCase @Inject constructor() { - operator fun invoke() = FirebaseAuth.getInstance().currentUser?.uid -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/user/SignOutUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/user/SignOutUseCase.kt deleted file mode 100644 index 9856fdfe..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/user/SignOutUseCase.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.squirtles.domain.usecase.user - -import com.google.firebase.auth.FirebaseAuth -import javax.inject.Inject - -class SignOutUseCase @Inject constructor() { - operator fun invoke() = FirebaseAuth.getInstance().signOut() -} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/user/UpdateUserNameUseCase.kt b/domain/src/main/java/com/squirtles/domain/usecase/user/UpdateUserNameUseCase.kt deleted file mode 100644 index c1ed3b3b..00000000 --- a/domain/src/main/java/com/squirtles/domain/usecase/user/UpdateUserNameUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.squirtles.domain.usecase.user - -import com.squirtles.domain.firebase.FirebaseRepository -import javax.inject.Inject - -class UpdateUserNameUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository -) { - suspend operator fun invoke(uid: String, newUserName: String) = - firebaseRepository.updateUserName(uid, newUserName) -} diff --git a/domain/user/.gitignore b/domain/user/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/user/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/user/build.gradle.kts b/domain/user/build.gradle.kts new file mode 100644 index 00000000..b3237734 --- /dev/null +++ b/domain/user/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id(libs.plugins.musicroad.java.library.get().pluginId) +} + +dependencies { + implementation(projects.core.model) + implementation(projects.domain.picklist) + implementation(projects.domain.pick) + implementation(projects.domain.favorite) + + implementation(libs.kotlinx.coroutines.core) +} diff --git a/domain/user/proguard-rules.pro b/domain/user/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/domain/user/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/user/src/main/java/com/squirtles/domain/user/FirebaseUserRepository.kt b/domain/user/src/main/java/com/squirtles/domain/user/FirebaseUserRepository.kt new file mode 100644 index 00000000..874dc995 --- /dev/null +++ b/domain/user/src/main/java/com/squirtles/domain/user/FirebaseUserRepository.kt @@ -0,0 +1,13 @@ +package com.squirtles.domain.user + +import com.squirtles.core.model.User + +interface FirebaseUserRepository { + val currentUser: String? + // user + fun signOut() + suspend fun createGoogleIdUser(uid: String, email: String, userName: String?, userProfileImage: String?): Result + suspend fun fetchUser(userId: String): Result + suspend fun updateUserName(userId: String, newUserName: String): Result + suspend fun deleteUser(uid: String): Result +} diff --git a/domain/user/src/main/java/com/squirtles/domain/user/LocalUserRepository.kt b/domain/user/src/main/java/com/squirtles/domain/user/LocalUserRepository.kt new file mode 100644 index 00000000..a26457e3 --- /dev/null +++ b/domain/user/src/main/java/com/squirtles/domain/user/LocalUserRepository.kt @@ -0,0 +1,13 @@ +package com.squirtles.domain.user + +import com.squirtles.core.model.User +import kotlinx.coroutines.flow.Flow + +interface LocalUserRepository { + val currentUser: User? + + fun readUserIdDataStore(): Flow + suspend fun saveUserIdDataStore(userId: String) + suspend fun saveCurrentUser(user: User) + suspend fun clearUser(): Result +} diff --git a/domain/src/main/java/com/squirtles/domain/usecase/user/CreateGoogleIdUserUseCase.kt b/domain/user/src/main/java/com/squirtles/domain/user/usecase/CreateGoogleIdUserUseCase.kt similarity index 54% rename from domain/src/main/java/com/squirtles/domain/usecase/user/CreateGoogleIdUserUseCase.kt rename to domain/user/src/main/java/com/squirtles/domain/user/usecase/CreateGoogleIdUserUseCase.kt index 5971fbf4..5a1b3b53 100644 --- a/domain/src/main/java/com/squirtles/domain/usecase/user/CreateGoogleIdUserUseCase.kt +++ b/domain/user/src/main/java/com/squirtles/domain/user/usecase/CreateGoogleIdUserUseCase.kt @@ -1,18 +1,18 @@ -package com.squirtles.domain.usecase.user +package com.squirtles.domain.user.usecase -import com.squirtles.domain.firebase.FirebaseRepository -import com.squirtles.domain.model.User +import com.squirtles.core.model.User +import com.squirtles.domain.user.FirebaseUserRepository import javax.inject.Inject class CreateGoogleIdUserUseCase @Inject constructor( - private val firebaseRepository: FirebaseRepository + private val firebaseUserRepository: FirebaseUserRepository ) { suspend operator fun invoke( uid: String, email: String, userName: String? = null, userProfileImage: String? = null - ): Result = firebaseRepository.createGoogleIdUser( + ): Result = firebaseUserRepository.createGoogleIdUser( uid, email, userName, diff --git a/domain/user/src/main/java/com/squirtles/domain/user/usecase/DeleteAccountUseCase.kt b/domain/user/src/main/java/com/squirtles/domain/user/usecase/DeleteAccountUseCase.kt new file mode 100644 index 00000000..0cdf572b --- /dev/null +++ b/domain/user/src/main/java/com/squirtles/domain/user/usecase/DeleteAccountUseCase.kt @@ -0,0 +1,45 @@ +package com.squirtles.domain.user.usecase + +import com.squirtles.domain.favorite.usecase.DeleteFavoriteUseCase +import com.squirtles.domain.pick.usecase.DeletePickUseCase +import com.squirtles.domain.pick.usecase.FetchFavoritePicksUseCase +import com.squirtles.domain.pick.usecase.FetchMyPicksUseCase +import com.squirtles.domain.user.FirebaseUserRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject + +class DeleteAccountUseCase @Inject constructor( + private val getCurrentUidUseCase: GetCurrentUidUseCase, + private val fetchFavoritePicksUseCase: FetchFavoritePicksUseCase, + private val deleteFavoriteUseCase: DeleteFavoriteUseCase, + private val fetchMyPicksUseCase: FetchMyPicksUseCase, + private val deletePickUseCase: DeletePickUseCase, + private val firebaseUserRepository: FirebaseUserRepository +) { + suspend operator fun invoke(): Result { + return runCatching { + val currentUid = getCurrentUidUseCase() + requireNotNull(currentUid) + coroutineScope { + // 1. 좋아한 픽 삭제 + val favoritePicks = fetchFavoritePicksUseCase(currentUid).getOrNull() ?: emptyList() + val favoritePicksDeleteJobs = favoritePicks.map { pick -> + async { deleteFavoriteUseCase(pick.id, currentUid) } + } + + // 2. 등록한 픽 삭제 + val myPicks = fetchMyPicksUseCase(currentUid).getOrNull() ?: emptyList() + val myPicksDeleteJobs = myPicks.map { pick -> + async { deletePickUseCase(pick.id, currentUid) } + } + + // 모든 삭제 작업이 끝날 때까지 기다림 + (favoritePicksDeleteJobs + myPicksDeleteJobs).awaitAll() + } + // 3. Firebase Firestore 유저 정보 삭제 + firebaseUserRepository.deleteUser(currentUid) + } + } +} diff --git a/domain/user/src/main/java/com/squirtles/domain/user/usecase/FetchUserByIdUseCase.kt b/domain/user/src/main/java/com/squirtles/domain/user/usecase/FetchUserByIdUseCase.kt new file mode 100644 index 00000000..f5295d0b --- /dev/null +++ b/domain/user/src/main/java/com/squirtles/domain/user/usecase/FetchUserByIdUseCase.kt @@ -0,0 +1,11 @@ +package com.squirtles.domain.user.usecase + +import com.squirtles.domain.user.FirebaseUserRepository +import javax.inject.Inject + +class FetchUserByIdUseCase @Inject constructor( + private val firebaseUserRepository: FirebaseUserRepository +) { + suspend operator fun invoke(userId: String) = + firebaseUserRepository.fetchUser(userId) +} diff --git a/domain/user/src/main/java/com/squirtles/domain/user/usecase/GetCurrentUidUseCase.kt b/domain/user/src/main/java/com/squirtles/domain/user/usecase/GetCurrentUidUseCase.kt new file mode 100644 index 00000000..c0c2d927 --- /dev/null +++ b/domain/user/src/main/java/com/squirtles/domain/user/usecase/GetCurrentUidUseCase.kt @@ -0,0 +1,10 @@ +package com.squirtles.domain.user.usecase + +import com.squirtles.domain.user.FirebaseUserRepository +import javax.inject.Inject + +class GetCurrentUidUseCase @Inject constructor( + private val firebaseUserRepository: FirebaseUserRepository +) { + operator fun invoke() = firebaseUserRepository.currentUser +} diff --git a/domain/user/src/main/java/com/squirtles/domain/user/usecase/SignOutUseCase.kt b/domain/user/src/main/java/com/squirtles/domain/user/usecase/SignOutUseCase.kt new file mode 100644 index 00000000..a3dc11f9 --- /dev/null +++ b/domain/user/src/main/java/com/squirtles/domain/user/usecase/SignOutUseCase.kt @@ -0,0 +1,10 @@ +package com.squirtles.domain.user.usecase + +import com.squirtles.domain.user.FirebaseUserRepository +import javax.inject.Inject + +class SignOutUseCase @Inject constructor( + private val firebaseUserRepository: FirebaseUserRepository +) { + operator fun invoke() = firebaseUserRepository.signOut() +} diff --git a/domain/user/src/main/java/com/squirtles/domain/user/usecase/UpdateUserNameUseCase.kt b/domain/user/src/main/java/com/squirtles/domain/user/usecase/UpdateUserNameUseCase.kt new file mode 100644 index 00000000..b79f4b99 --- /dev/null +++ b/domain/user/src/main/java/com/squirtles/domain/user/usecase/UpdateUserNameUseCase.kt @@ -0,0 +1,11 @@ +package com.squirtles.domain.user.usecase + +import com.squirtles.domain.user.FirebaseUserRepository +import javax.inject.Inject + +class UpdateUserNameUseCase @Inject constructor( + private val firebaseUserRepository: FirebaseUserRepository +) { + suspend operator fun invoke(userId: String, newUserName: String) = + firebaseUserRepository.updateUserName(userId, newUserName) +} diff --git a/feature/create/.gitignore b/feature/create/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/create/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/create/build.gradle.kts b/feature/create/build.gradle.kts new file mode 100644 index 00000000..498d8346 --- /dev/null +++ b/feature/create/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.musicroad.feature) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.squirtles.feature.create" +} + +dependencies { + implementation(projects.core.common) + + implementation(projects.domain.pick) + implementation(projects.domain.user) + implementation(projects.domain.applemusic) + implementation(projects.domain.location) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + // Serialization + implementation(libs.kotlinx.serialization.json) +} diff --git a/feature/create/proguard-rules.pro b/feature/create/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/create/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/src/androidTest/java/com/squirtles/domain/ExampleInstrumentedTest.kt b/feature/create/src/androidTest/java/com/squirtles/feature/create/ExampleInstrumentedTest.kt similarity index 85% rename from domain/src/androidTest/java/com/squirtles/domain/ExampleInstrumentedTest.kt rename to feature/create/src/androidTest/java/com/squirtles/feature/create/ExampleInstrumentedTest.kt index 7871980c..c0e7429a 100644 --- a/domain/src/androidTest/java/com/squirtles/domain/ExampleInstrumentedTest.kt +++ b/feature/create/src/androidTest/java/com/squirtles/feature/create/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package com.squirtles.domain +package com.squirtles.create import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.squirtles.domain.test", appContext.packageName) + assertEquals("com.squirtles.create.test", appContext.packageName) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/squirtles/musicroad/create/CreatePickScreen.kt b/feature/create/src/main/java/com/squirtles/feature/create/CreatePickScreen.kt similarity index 96% rename from app/src/main/java/com/squirtles/musicroad/create/CreatePickScreen.kt rename to feature/create/src/main/java/com/squirtles/feature/create/CreatePickScreen.kt index 21c7bbb6..f2a8f1c3 100644 --- a/app/src/main/java/com/squirtles/musicroad/create/CreatePickScreen.kt +++ b/feature/create/src/main/java/com/squirtles/feature/create/CreatePickScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.create +package com.squirtles.feature.create import android.app.Activity import android.util.Size @@ -56,14 +56,13 @@ import androidx.core.graphics.toColorInt import androidx.core.view.WindowInsetsControllerCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.AlbumImage -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.Dark -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.AlbumImage +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.Dark +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.model.Song @Composable fun CreatePickScreen( diff --git a/app/src/main/java/com/squirtles/musicroad/create/CreatePickUiState.kt b/feature/create/src/main/java/com/squirtles/feature/create/CreatePickUiState.kt similarity index 55% rename from app/src/main/java/com/squirtles/musicroad/create/CreatePickUiState.kt rename to feature/create/src/main/java/com/squirtles/feature/create/CreatePickUiState.kt index 7ec0c6b6..ca1e61b7 100644 --- a/app/src/main/java/com/squirtles/musicroad/create/CreatePickUiState.kt +++ b/feature/create/src/main/java/com/squirtles/feature/create/CreatePickUiState.kt @@ -1,9 +1,4 @@ -package com.squirtles.musicroad.create - -sealed class SearchUiState { - data object HotResult : SearchUiState() - data object SearchResult : SearchUiState() -} +package com.squirtles.feature.create sealed class CreateUiState { data object Default : CreateUiState() diff --git a/app/src/main/java/com/squirtles/musicroad/create/CreatePickViewModel.kt b/feature/create/src/main/java/com/squirtles/feature/create/CreatePickViewModel.kt similarity index 83% rename from app/src/main/java/com/squirtles/musicroad/create/CreatePickViewModel.kt rename to feature/create/src/main/java/com/squirtles/feature/create/CreatePickViewModel.kt index b4202e38..3f115235 100644 --- a/app/src/main/java/com/squirtles/musicroad/create/CreatePickViewModel.kt +++ b/feature/create/src/main/java/com/squirtles/feature/create/CreatePickViewModel.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.create +package com.squirtles.feature.create import android.location.Location import android.util.Log @@ -6,23 +6,25 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import com.squirtles.domain.model.Creator -import com.squirtles.domain.model.LocationPoint -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.Song -import com.squirtles.domain.usecase.location.GetLastLocationUseCase -import com.squirtles.domain.usecase.music.FetchMusicVideoUseCase -import com.squirtles.domain.usecase.mypick.CreatePickUseCase -import com.squirtles.domain.usecase.user.FetchUserByIdUseCase -import com.squirtles.domain.usecase.user.GetCurrentUidUseCase -import com.squirtles.musicroad.navigation.SearchRoute -import com.squirtles.musicroad.utils.throttleFirst +import com.squirtles.domain.applemusic.usecase.FetchMusicVideoUseCase +import com.squirtles.domain.location.usecase.GetLastLocationUseCase +import com.squirtles.core.model.Creator +import com.squirtles.core.model.LocationPoint +import com.squirtles.core.model.Pick +import com.squirtles.core.model.Song +import com.squirtles.core.navigation.SearchRoute +import com.squirtles.domain.pick.usecase.CreatePickUseCase +import com.squirtles.domain.user.usecase.FetchUserByIdUseCase +import com.squirtles.domain.user.usecase.GetCurrentUidUseCase +import com.squirtles.core.util.serializableType +import com.squirtles.core.util.throttleFirst import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.reflect.typeOf @HiltViewModel class CreatePickViewModel @Inject constructor( @@ -34,7 +36,8 @@ class CreatePickViewModel @Inject constructor( private val fetchUserByIdUseCase: FetchUserByIdUseCase ) : ViewModel() { - private val song = savedStateHandle.toRoute(SearchRoute.Create.typeMap).song + private val typeMap = mapOf(typeOf() to serializableType()) + private val song = savedStateHandle.toRoute(typeMap).song private val _createPickUiState = MutableStateFlow>(CreateUiState.Default) val createPickUiState = _createPickUiState.asStateFlow() diff --git a/app/src/main/java/com/squirtles/musicroad/search/navigation/SearchNavigation.kt b/feature/create/src/main/java/com/squirtles/feature/create/navigation/CreateNavigation.kt similarity index 62% rename from app/src/main/java/com/squirtles/musicroad/search/navigation/SearchNavigation.kt rename to feature/create/src/main/java/com/squirtles/feature/create/navigation/CreateNavigation.kt index b46e2049..7dc565bf 100644 --- a/app/src/main/java/com/squirtles/musicroad/search/navigation/SearchNavigation.kt +++ b/feature/create/src/main/java/com/squirtles/feature/create/navigation/CreateNavigation.kt @@ -1,21 +1,17 @@ -package com.squirtles.musicroad.search.navigation +package com.squirtles.feature.create.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.create.CreatePickScreen -import com.squirtles.musicroad.navigation.MainRoute -import com.squirtles.musicroad.navigation.SearchRoute -import com.squirtles.musicroad.search.SearchMusicScreen +import com.squirtles.feature.create.CreatePickScreen +import com.squirtles.core.model.Song +import com.squirtles.core.navigation.SearchRoute +import com.squirtles.core.util.serializableType import java.net.URLEncoder import java.nio.charset.StandardCharsets - -fun NavController.navigateSearch(navOptions: NavOptions? = null) { - navigate(MainRoute.Search, navOptions) -} +import kotlin.reflect.typeOf fun NavController.navigateCreate(song: Song, navOptions: NavOptions? = null) { val encodedSong = song.copy( @@ -27,19 +23,12 @@ fun NavController.navigateCreate(song: Song, navOptions: NavOptions? = null) { navigate(SearchRoute.Create(encodedSong), navOptions) } -fun NavGraphBuilder.searchNavGraph( +fun NavGraphBuilder.createNavGraph( onBackClick: () -> Unit, - onItemClick: (Song) -> Unit, onCreateClick: (String) -> Unit ) { - composable { - SearchMusicScreen( - onBackClick = onBackClick, - onItemClick = onItemClick, // Create 이동 - ) - } composable( - typeMap = SearchRoute.Create.typeMap + typeMap = mapOf(typeOf() to serializableType()) ) { backStackEntry -> val song = backStackEntry.toRoute().song diff --git a/feature/create/src/main/res/values/strings.xml b/feature/create/src/main/res/values/strings.xml new file mode 100644 index 00000000..c0ae7751 --- /dev/null +++ b/feature/create/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + MusicRoad + + 상단 바 뒤로 가기 버튼 + 앨범 이미지 + 거리에 남길 한마디를 입력하세요. + 픽 등록 + 등록하기 + diff --git a/feature/create/src/test/java/com/squirtles/feature/create/ExampleUnitTest.kt b/feature/create/src/test/java/com/squirtles/feature/create/ExampleUnitTest.kt new file mode 100644 index 00000000..4edd7162 --- /dev/null +++ b/feature/create/src/test/java/com/squirtles/feature/create/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.squirtles.create + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/feature/detail/.gitignore b/feature/detail/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/detail/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/detail/build.gradle.kts b/feature/detail/build.gradle.kts new file mode 100644 index 00000000..86e2db21 --- /dev/null +++ b/feature/detail/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + alias(libs.plugins.musicroad.feature) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.squirtles.feature.detail" +} + +dependencies { + implementation(projects.audioVisualizer) + implementation(projects.core.account) + implementation(projects.core.musicplayer) + implementation(projects.domain.pick) + implementation(projects.domain.picklist) + implementation(projects.domain.user) + implementation(projects.domain.favorite) + + implementation(libs.coil.compose) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.googleid) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + // Serialization + implementation(libs.kotlinx.serialization.json) +} diff --git a/feature/detail/proguard-rules.pro b/feature/detail/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/detail/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/detail/src/androidTest/java/com/squirtles/feature/detail/ExampleInstrumentedTest.kt b/feature/detail/src/androidTest/java/com/squirtles/feature/detail/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..b2b4c3a0 --- /dev/null +++ b/feature/detail/src/androidTest/java/com/squirtles/feature/detail/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.squirtles.detail + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.squirtles.detail.test", appContext.packageName) + } +} diff --git a/app/src/main/java/com/squirtles/musicroad/detail/DetailViewModel.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/DetailViewModel.kt similarity index 84% rename from app/src/main/java/com/squirtles/musicroad/detail/DetailViewModel.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/DetailViewModel.kt index 8de20687..6637e5f7 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/DetailViewModel.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/DetailViewModel.kt @@ -1,20 +1,19 @@ -package com.squirtles.musicroad.detail +package com.squirtles.feature.detail import androidx.core.graphics.toColorInt -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.squirtles.domain.model.Creator -import com.squirtles.domain.model.LocationPoint -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.Song -import com.squirtles.domain.usecase.favorite.CreateFavoriteUseCase -import com.squirtles.domain.usecase.favorite.DeleteFavoriteUseCase -import com.squirtles.domain.usecase.mypick.DeletePickUseCase -import com.squirtles.domain.usecase.pick.FetchIsFavoriteUseCase -import com.squirtles.domain.usecase.pick.FetchPickUseCase -import com.squirtles.domain.usecase.user.GetCurrentUidUseCase -import com.squirtles.musicroad.utils.throttleFirst +import com.squirtles.domain.favorite.usecase.CreateFavoriteUseCase +import com.squirtles.domain.favorite.usecase.DeleteFavoriteUseCase +import com.squirtles.domain.favorite.usecase.FetchIsFavoriteUseCase +import com.squirtles.core.model.Creator +import com.squirtles.core.model.LocationPoint +import com.squirtles.core.model.Pick +import com.squirtles.core.model.Song +import com.squirtles.domain.pick.usecase.DeletePickUseCase +import com.squirtles.domain.pick.usecase.FetchPickUseCase +import com.squirtles.domain.user.usecase.GetCurrentUidUseCase +import com.squirtles.core.util.throttleFirst import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow @@ -26,7 +25,6 @@ import javax.inject.Inject @HiltViewModel class DetailViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, private val fetchPickUseCase: FetchPickUseCase, private val fetchIsFavoriteUseCase: FetchIsFavoriteUseCase, private val getCurrentUidUseCase: GetCurrentUidUseCase, @@ -53,9 +51,9 @@ class DetailViewModel @Inject constructor( .collect { (pickId, isAdding) -> getUid()?.let { uid -> if (isAdding) { - addToFavoritePicks(pickId, uid) + addFavorite(pickId, uid) } else { - deleteFromFavoritePicks(pickId, uid) + deleteFavorite(pickId, uid) } } } @@ -113,7 +111,7 @@ class DetailViewModel @Inject constructor( } } - private fun addToFavoritePicks(pickId: String, uid: String) { + private fun addFavorite(pickId: String, uid: String) { viewModelScope.launch { createFavoriteUseCase(pickId, uid) .onSuccess { @@ -129,7 +127,7 @@ class DetailViewModel @Inject constructor( } } - private fun deleteFromFavoritePicks(pickId: String, uid: String) { + private fun deleteFavorite(pickId: String, uid: String) { viewModelScope.launch { deleteFavoriteUseCase(pickId, uid) .onSuccess { diff --git a/app/src/main/java/com/squirtles/musicroad/detail/FavoriteAction.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/FavoriteAction.kt similarity index 56% rename from app/src/main/java/com/squirtles/musicroad/detail/FavoriteAction.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/FavoriteAction.kt index 8169095b..4b484f73 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/FavoriteAction.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/FavoriteAction.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail +package com.squirtles.feature.detail enum class FavoriteAction { ADDED, DELETED diff --git a/app/src/main/java/com/squirtles/musicroad/detail/PickDetailScreen.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/PickDetailScreen.kt similarity index 91% rename from app/src/main/java/com/squirtles/musicroad/detail/PickDetailScreen.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/PickDetailScreen.kt index b7831dde..b52033fd 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/PickDetailScreen.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/PickDetailScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail +package com.squirtles.feature.detail import android.app.Activity import android.content.Context @@ -54,29 +54,27 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.squirtles.domain.model.Pick -import com.squirtles.musicroad.R -import com.squirtles.musicroad.account.AccountViewModel -import com.squirtles.musicroad.account.GoogleId -import com.squirtles.musicroad.common.DialogTextButton -import com.squirtles.musicroad.common.HorizontalSpacer -import com.squirtles.musicroad.common.MessageAlertDialog -import com.squirtles.musicroad.common.SignInAlertDialog -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.detail.DetailViewModel.Companion.DEFAULT_PICK -import com.squirtles.musicroad.detail.components.CircleAlbumCover -import com.squirtles.musicroad.detail.components.CommentText -import com.squirtles.musicroad.detail.components.DetailPickTopAppBar -import com.squirtles.musicroad.detail.components.MusicVideoKnob -import com.squirtles.musicroad.detail.components.PickInformation -import com.squirtles.musicroad.detail.components.SongInfo -import com.squirtles.musicroad.detail.components.music.MusicPlayer -import com.squirtles.musicroad.detail.components.music.visualizer.BaseVisualizer -import com.squirtles.musicroad.media.PlayerServiceViewModel -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White -import com.squirtles.musicroad.videoplayer.MusicVideoScreen +import com.squirtles.core.account.AccountViewModel +import com.squirtles.core.account.GoogleId +import com.squirtles.core.common.ui.DialogTextButton +import com.squirtles.core.common.ui.HorizontalSpacer +import com.squirtles.core.common.ui.MessageAlertDialog +import com.squirtles.core.common.ui.SignInAlertDialog +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.detail.DetailViewModel.Companion.DEFAULT_PICK +import com.squirtles.feature.detail.components.CircleAlbumCover +import com.squirtles.feature.detail.components.PickCommentText +import com.squirtles.feature.detail.components.DetailPickTopAppBar +import com.squirtles.feature.detail.components.MusicVideoKnob +import com.squirtles.feature.detail.components.PickInformation +import com.squirtles.feature.detail.components.SongInfo +import com.squirtles.feature.detail.components.music.MusicPlayer +import com.squirtles.feature.detail.videoplayer.MusicVideoScreen +import com.squirtles.core.model.Pick +import com.squirtles.core.musicplayer.PlayerServiceViewModel import kotlinx.coroutines.launch import kotlin.math.absoluteValue @@ -135,7 +133,6 @@ fun PickDetailScreen( } is PickDetailUiState.Success -> { - val lifecycleOwner = LocalLifecycleOwner.current val pick = (uiState as PickDetailUiState.Success).pick val isFavorite = (uiState as PickDetailUiState.Success).isFavorite val isNonMember = detailViewModel.getUid() == null @@ -389,8 +386,6 @@ private fun PickDetailContents( val onDynamicBackgroundColor = if (dynamicBackgroundColor.luminance() >= 0.5f) Black else White val view = LocalView.current - val baseVisualizer = remember { BaseVisualizer() } - val audioEffectColor = dynamicBackgroundColor.copy( red = (dynamicBackgroundColor.red + 0.2f).coerceAtMost(1.0f), green = (dynamicBackgroundColor.green + 0.2f).coerceAtMost(1.0f), @@ -474,7 +469,6 @@ private fun PickDetailContents( currentPosition = { playerUiState.currentPosition }, duration = { playerUiState.duration }, audioEffectColor = audioEffectColor, - baseVisualizer = { baseVisualizer }, audioSessionId = audioSessionId, onSeekChanged = { timeMs -> playerServiceViewModel.onSeekingFinished(timeMs) @@ -495,7 +489,7 @@ private fun PickDetailContents( favoriteCount = favoriteCount ) - CommentText(comment = pick.comment) + PickCommentText(comment = pick.comment) VerticalSpacer(height = 8) } @@ -514,8 +508,8 @@ private fun PickDetailContents( playerServiceViewModel.onRewindBy() } }, - onPauseToggle = { song -> - playerServiceViewModel.togglePlayPause(song) + onPauseToggle = { _ -> + playerServiceViewModel.togglePlayPause() }, ) } diff --git a/app/src/main/java/com/squirtles/musicroad/detail/PickDetailUiState.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/PickDetailUiState.kt similarity index 76% rename from app/src/main/java/com/squirtles/musicroad/detail/PickDetailUiState.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/PickDetailUiState.kt index a55c5b86..6ef49820 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/PickDetailUiState.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/PickDetailUiState.kt @@ -1,6 +1,6 @@ -package com.squirtles.musicroad.detail +package com.squirtles.feature.detail -import com.squirtles.domain.model.Pick +import com.squirtles.core.model.Pick sealed class PickDetailUiState { data object Loading : PickDetailUiState() diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/CircleAlbumCover.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/CircleAlbumCover.kt similarity index 67% rename from app/src/main/java/com/squirtles/musicroad/detail/components/CircleAlbumCover.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/CircleAlbumCover.kt index ab5a4868..29cfeac7 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/CircleAlbumCover.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/CircleAlbumCover.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components +package com.squirtles.feature.detail.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio @@ -15,19 +15,20 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.R -import com.squirtles.musicroad.detail.components.music.visualizer.BaseVisualizer -import com.squirtles.musicroad.detail.components.music.visualizer.CircleVisualizer +import com.miller198.audiovisualizer.configs.GradientConfig +import com.miller198.audiovisualizer.configs.VisualizerConfig +import com.miller198.audiovisualizer.soundeffect.SoundEffects +import com.miller198.audiovisualizer.ui.CircleVisualizer +import com.squirtles.core.model.Song +import com.squirtles.feature.detail.R @Composable internal fun CircleAlbumCover( + audioSessionId: Int, song: Song, currentPosition: () -> Long, duration: () -> Long, audioEffectColor: Color, - baseVisualizer: () -> BaseVisualizer, - audioSessionId: Int, onSeekChanged: (Long) -> Unit, modifier: Modifier = Modifier, ) { @@ -35,15 +36,19 @@ internal fun CircleAlbumCover( modifier = modifier ) { CircleVisualizer( - baseVisualizer = baseVisualizer, audioSessionId = audioSessionId, + soundEffects = SoundEffects.BAR, + visualizerConfig = VisualizerConfig.FftCaptureConfig.Default, + gradientConfig = GradientConfig.Enabled( + color = audioEffectColor.mixedWhite(), + duration = 2500 + ), color = audioEffectColor, - sizeRatio = 0.5f, - modifier = Modifier.align(Alignment.Center) + modifier = modifier.align(Alignment.Center) ) PlayCircularProgressIndicator( - modifier = Modifier + modifier = modifier .fillMaxSize() .padding(10.dp) .align(Alignment.Center), @@ -68,3 +73,10 @@ internal fun CircleAlbumCover( ) } } + +private fun Color.mixedWhite(): Color = Color( + red = (Color.White.red + this.red) / 2, + green = (Color.White.green + this.green) / 2, + blue = (Color.White.blue + this.blue) / 2, + alpha = 0.9f +) diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/DetailPickTopAppBar.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/DetailPickTopAppBar.kt similarity index 95% rename from app/src/main/java/com/squirtles/musicroad/detail/components/DetailPickTopAppBar.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/DetailPickTopAppBar.kt index a4e85e96..9229b0c0 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/DetailPickTopAppBar.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/DetailPickTopAppBar.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components +package com.squirtles.feature.detail.components import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -19,9 +19,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.CreatedByOtherUserText -import com.squirtles.musicroad.common.CreatedBySelfText +import com.squirtles.core.common.ui.CreatedByOtherUserText +import com.squirtles.core.common.ui.CreatedBySelfText +import com.squirtles.feature.detail.R @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/MusicVideoKnob.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/MusicVideoKnob.kt similarity index 93% rename from app/src/main/java/com/squirtles/musicroad/detail/components/MusicVideoKnob.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/MusicVideoKnob.kt index 8d7ca3b0..434dd522 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/MusicVideoKnob.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/MusicVideoKnob.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components +package com.squirtles.feature.detail.components import android.util.Size import androidx.compose.animation.core.FastOutSlowInEasing @@ -26,9 +26,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.request.ImageRequest -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.toImageUrlWithSize -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.common.ui.toImageUrlWithSize +import com.squirtles.feature.detail.R @Composable internal fun MusicVideoKnob( diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/CommentText.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/PickCommentText.kt similarity index 82% rename from app/src/main/java/com/squirtles/musicroad/detail/components/CommentText.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/PickCommentText.kt index bf3a830e..be9c08e6 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/CommentText.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/PickCommentText.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components +package com.squirtles.feature.detail.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -16,13 +16,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.Dark -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.theme.Dark +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.detail.R @Composable -internal fun CommentText( +internal fun PickCommentText( comment: String, modifier: Modifier = Modifier ) { @@ -44,11 +44,11 @@ internal fun CommentText( @Composable private fun CommentTextPreview() { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - CommentText(comment = "") + PickCommentText(comment = "") - CommentText(comment = "노래가 좋아서 추천합니다.") + PickCommentText(comment = "노래가 좋아서 추천합니다.") - CommentText( + PickCommentText( comment = "노래가 너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무" + "너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무너무 좋아요" ) diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/PickInformation.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/PickInformation.kt similarity index 91% rename from app/src/main/java/com/squirtles/musicroad/detail/components/PickInformation.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/PickInformation.kt index 51a6b925..f490bdcb 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/PickInformation.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/PickInformation.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components +package com.squirtles.feature.detail.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -13,8 +13,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.Gray +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.feature.detail.R @Composable internal fun PickInformation(formattedDate: String, favoriteCount: Int) { diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/PlayCircularProgressIndicator.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/PlayCircularProgressIndicator.kt similarity index 96% rename from app/src/main/java/com/squirtles/musicroad/detail/components/PlayCircularProgressIndicator.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/PlayCircularProgressIndicator.kt index d9d668a0..b8f42b54 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/PlayCircularProgressIndicator.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/PlayCircularProgressIndicator.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components +package com.squirtles.feature.detail.components import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box @@ -12,7 +12,7 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.theme.White import kotlin.math.atan2 import kotlin.math.hypot diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/SongInfo.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/SongInfo.kt similarity index 92% rename from app/src/main/java/com/squirtles/musicroad/detail/components/SongInfo.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/SongInfo.kt index 149104a5..1569ce13 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/SongInfo.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/SongInfo.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components +package com.squirtles.feature.detail.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import com.squirtles.domain.model.Song +import com.squirtles.core.model.Song @Composable internal fun SongInfo( diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/SwipeUpIcon.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/SwipeUpIcon.kt similarity index 87% rename from app/src/main/java/com/squirtles/musicroad/detail/components/SwipeUpIcon.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/SwipeUpIcon.kt index c9b4f063..010ceff6 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/SwipeUpIcon.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/SwipeUpIcon.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components +package com.squirtles.feature.detail.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -10,8 +10,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.detail.R @Composable internal fun SwipeUpIcon( diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/music/MusicPlayer.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/music/MusicPlayer.kt similarity index 86% rename from app/src/main/java/com/squirtles/musicroad/detail/components/music/MusicPlayer.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/music/MusicPlayer.kt index 8f49325b..f1a03cf3 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/music/MusicPlayer.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/music/MusicPlayer.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components.music +package com.squirtles.feature.detail.components.music import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -9,10 +9,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.squirtles.domain.model.PlayerState -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.common.Constants.DEFAULT_PADDING -import com.squirtles.musicroad.ui.theme.PlayerBackground +import com.squirtles.core.common.ui.Constants.DEFAULT_PADDING +import com.squirtles.core.common.ui.theme.PlayerBackground +import com.squirtles.core.model.PlayerState +import com.squirtles.core.model.Song @Composable fun MusicPlayer( diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/music/PlayBar.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/music/PlayBar.kt similarity index 94% rename from app/src/main/java/com/squirtles/musicroad/detail/components/music/PlayBar.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/music/PlayBar.kt index e4d91cf3..1f4bc0a9 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/music/PlayBar.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/music/PlayBar.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components.music +package com.squirtles.feature.detail.components.music import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -11,8 +11,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.MusicRoadTheme import java.util.concurrent.TimeUnit @Composable diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/music/PlayProgressIndicator.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/music/PlayProgressIndicator.kt similarity index 91% rename from app/src/main/java/com/squirtles/musicroad/detail/components/music/PlayProgressIndicator.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/music/PlayProgressIndicator.kt index b0586e02..acc1f498 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/music/PlayProgressIndicator.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/music/PlayProgressIndicator.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components.music +package com.squirtles.feature.detail.components.music import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box @@ -10,8 +10,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.ui.theme.DarkGray -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.theme.DarkGray +import com.squirtles.core.common.ui.theme.White @Composable internal fun PlayProgressIndicator( diff --git a/app/src/main/java/com/squirtles/musicroad/detail/components/music/PlayerControls.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/components/music/PlayerControls.kt similarity index 93% rename from app/src/main/java/com/squirtles/musicroad/detail/components/music/PlayerControls.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/components/music/PlayerControls.kt index d24f0d45..076a0360 100644 --- a/app/src/main/java/com/squirtles/musicroad/detail/components/music/PlayerControls.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/components/music/PlayerControls.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.detail.components.music +package com.squirtles.feature.detail.components.music import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -16,9 +16,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.ui.theme.MusicRoadTheme -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.detail.R @Composable internal fun PlayerControls( diff --git a/feature/detail/src/main/java/com/squirtles/feature/detail/navigation/PickDetailNavigation.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/navigation/PickDetailNavigation.kt new file mode 100644 index 00000000..6bbe06d0 --- /dev/null +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/navigation/PickDetailNavigation.kt @@ -0,0 +1,34 @@ +package com.squirtles.feature.detail.navigation + +import android.content.Context +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.squirtles.feature.detail.PickDetailScreen +import com.squirtles.core.musicplayer.PlayerServiceViewModel +import com.squirtles.core.navigation.MapRoute + +fun NavController.navigatePickDetail(pickId: String, navOptions: NavOptions? = null) { + navigate(MapRoute.PickDetail(pickId), navOptions) +} + +fun NavGraphBuilder.detailNavGraph( + playerServiceViewModel: PlayerServiceViewModel, + onUserInfoClick: (String) -> Unit, + onBackClick: () -> Unit, + onDeleted: (Context) -> Unit, +) { + composable { backStackEntry -> + val pickId = backStackEntry.toRoute().pickId + + PickDetailScreen( + pickId = pickId, + playerServiceViewModel = playerServiceViewModel, + onUserInfoClick = onUserInfoClick, + onBackClick = onBackClick, + onDeleted = onDeleted, + ) + } +} diff --git a/app/src/main/java/com/squirtles/musicroad/videoplayer/MusicVideoPlayer.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/MusicVideoPlayer.kt similarity index 94% rename from app/src/main/java/com/squirtles/musicroad/videoplayer/MusicVideoPlayer.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/MusicVideoPlayer.kt index 0146b7d3..7bb636ad 100644 --- a/app/src/main/java/com/squirtles/musicroad/videoplayer/MusicVideoPlayer.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/MusicVideoPlayer.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.videoplayer +package com.squirtles.feature.detail.videoplayer import android.graphics.Matrix import android.graphics.SurfaceTexture @@ -20,14 +20,13 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import androidx.media3.common.util.UnstableApi -import com.squirtles.domain.model.Pick +import dagger.hilt.android.UnstableApi import kotlinx.coroutines.launch @OptIn(UnstableApi::class) @Composable fun MusicVideoPlayer( - pick: Pick, + musicVideoUrl: String, videoPlayerViewModel: VideoPlayerViewModel = hiltViewModel() ) { val context = LocalContext.current @@ -56,7 +55,7 @@ fun MusicVideoPlayer( AndroidView( factory = { - videoPlayerViewModel.initializePlayer(context, pick.musicVideoUrl) + videoPlayerViewModel.initializePlayer(context, musicVideoUrl) textureView.apply { surfaceTextureListener = object : TextureView.SurfaceTextureListener { override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) { diff --git a/app/src/main/java/com/squirtles/musicroad/videoplayer/MusicVideoScreen.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/MusicVideoScreen.kt similarity index 85% rename from app/src/main/java/com/squirtles/musicroad/videoplayer/MusicVideoScreen.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/MusicVideoScreen.kt index 3efab07e..38068022 100644 --- a/app/src/main/java/com/squirtles/musicroad/videoplayer/MusicVideoScreen.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/MusicVideoScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.videoplayer +package com.squirtles.feature.detail.videoplayer import androidx.activity.compose.BackHandler import androidx.annotation.OptIn @@ -11,8 +11,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.media3.common.util.UnstableApi -import com.squirtles.domain.model.Pick +import com.squirtles.core.model.Pick +import dagger.hilt.android.UnstableApi @OptIn(UnstableApi::class) @Composable @@ -27,7 +27,7 @@ fun MusicVideoScreen( BackHandler { onBackClick() } Box(modifier = modifier.fillMaxSize()) { - MusicVideoPlayer(pick) + MusicVideoPlayer(pick.musicVideoUrl) VideoPlayerOverlay(pick, onBackClick) if (isLoading) { diff --git a/app/src/main/java/com/squirtles/musicroad/videoplayer/VideoPlayerOverlay.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/VideoPlayerOverlay.kt similarity index 95% rename from app/src/main/java/com/squirtles/musicroad/videoplayer/VideoPlayerOverlay.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/VideoPlayerOverlay.kt index 4d06a83d..1e35abe2 100644 --- a/app/src/main/java/com/squirtles/musicroad/videoplayer/VideoPlayerOverlay.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/VideoPlayerOverlay.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.videoplayer +package com.squirtles.feature.detail.videoplayer import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween @@ -47,15 +47,15 @@ import androidx.compose.ui.unit.sp import androidx.core.graphics.toColorInt import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.squirtles.domain.model.Creator -import com.squirtles.domain.model.LocationPoint -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.model.Creator +import com.squirtles.core.model.LocationPoint +import com.squirtles.core.model.Pick +import com.squirtles.core.model.Song +import com.squirtles.feature.detail.R @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/squirtles/musicroad/videoplayer/VideoPlayerState.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/VideoPlayerState.kt similarity index 53% rename from app/src/main/java/com/squirtles/musicroad/videoplayer/VideoPlayerState.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/VideoPlayerState.kt index 2036fa3d..35680f07 100644 --- a/app/src/main/java/com/squirtles/musicroad/videoplayer/VideoPlayerState.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/VideoPlayerState.kt @@ -1,5 +1,5 @@ -package com.squirtles.musicroad.videoplayer +package com.squirtles.feature.detail.videoplayer enum class VideoPlayerState { Playing, Pause, Replay -} \ No newline at end of file +} diff --git a/app/src/main/java/com/squirtles/musicroad/videoplayer/VideoPlayerViewModel.kt b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/VideoPlayerViewModel.kt similarity index 97% rename from app/src/main/java/com/squirtles/musicroad/videoplayer/VideoPlayerViewModel.kt rename to feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/VideoPlayerViewModel.kt index d9493642..b91a39e8 100644 --- a/app/src/main/java/com/squirtles/musicroad/videoplayer/VideoPlayerViewModel.kt +++ b/feature/detail/src/main/java/com/squirtles/feature/detail/videoplayer/VideoPlayerViewModel.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.videoplayer +package com.squirtles.feature.detail.videoplayer import android.content.Context import android.util.Size @@ -11,7 +11,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.VideoSize import androidx.media3.exoplayer.ExoPlayer -import com.squirtles.musicroad.R +import com.squirtles.feature.detail.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/app/src/main/res/drawable/ic_delete.xml b/feature/detail/src/main/res/drawable/ic_delete.xml similarity index 100% rename from app/src/main/res/drawable/ic_delete.xml rename to feature/detail/src/main/res/drawable/ic_delete.xml diff --git a/feature/detail/src/main/res/drawable/ic_favorite.xml b/feature/detail/src/main/res/drawable/ic_favorite.xml new file mode 100644 index 00000000..67b2268e --- /dev/null +++ b/feature/detail/src/main/res/drawable/ic_favorite.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_favorite_false.xml b/feature/detail/src/main/res/drawable/ic_favorite_false.xml similarity index 100% rename from app/src/main/res/drawable/ic_favorite_false.xml rename to feature/detail/src/main/res/drawable/ic_favorite_false.xml diff --git a/app/src/main/res/drawable/ic_favorite_true.xml b/feature/detail/src/main/res/drawable/ic_favorite_true.xml similarity index 100% rename from app/src/main/res/drawable/ic_favorite_true.xml rename to feature/detail/src/main/res/drawable/ic_favorite_true.xml diff --git a/app/src/main/res/drawable/ic_swipe.xml b/feature/detail/src/main/res/drawable/ic_swipe.xml similarity index 100% rename from app/src/main/res/drawable/ic_swipe.xml rename to feature/detail/src/main/res/drawable/ic_swipe.xml diff --git a/feature/detail/src/main/res/values/strings.xml b/feature/detail/src/main/res/values/strings.xml new file mode 100644 index 00000000..e5fad94b --- /dev/null +++ b/feature/detail/src/main/res/values/strings.xml @@ -0,0 +1,39 @@ + + + + 픽을 담기 위해\n로그인이 필요합니다 + 삭제되었습니다 + 픽 보관함에 추가되었습니다. + 픽 보관함에서 제거되었습니다. + 일시적인 오류가 발생했습니다. + + 삭제하시겠습니까? + 등록하신 픽이 삭제됩니다. + 취소 + 삭제하기 + + 픽을 담은 개수 + 등록된 한마디가 없습니다. + 앨범 이미지 + + + 님의 픽 + 뒤로 가기 + 재생/일시정지 + 480p + + + 밀어서 뮤직비디오 보기 + Apple Music에서 제공하는 30초 미리보기 영상입니다. + + + 상단 바 뒤로 가기 버튼 + 삭제하기 + 담은 픽 + 픽 담기 + + + 5초 뒤로 + 5초 앞으로 + + diff --git a/feature/detail/src/test/java/com/squirtles/feature/detail/ExampleUnitTest.kt b/feature/detail/src/test/java/com/squirtles/feature/detail/ExampleUnitTest.kt new file mode 100644 index 00000000..c84953f2 --- /dev/null +++ b/feature/detail/src/test/java/com/squirtles/feature/detail/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.squirtles.detail + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/feature/favorite/.gitignore b/feature/favorite/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/favorite/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/favorite/build.gradle.kts b/feature/favorite/build.gradle.kts new file mode 100644 index 00000000..864f46a7 --- /dev/null +++ b/feature/favorite/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.musicroad.feature) +} + +android { + namespace = "com.squirtles.feature.favorite" +} + +dependencies { + implementation(projects.core.picklist) + implementation(projects.domain.picklist) + implementation(projects.domain.favorite) + implementation(projects.domain.pick) + implementation(projects.domain.order) + implementation(projects.domain.user) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) +} diff --git a/feature/favorite/proguard-rules.pro b/feature/favorite/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/favorite/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/favorite/src/androidTest/java/com/squirtles/feature/favorite/ExampleInstrumentedTest.kt b/feature/favorite/src/androidTest/java/com/squirtles/feature/favorite/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..ee9b49aa --- /dev/null +++ b/feature/favorite/src/androidTest/java/com/squirtles/feature/favorite/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.squirtles.feature.favorite + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.squirtles.favorite.test", appContext.packageName) + } +} diff --git a/app/src/main/java/com/squirtles/musicroad/favorite/FavoriteListViewModel.kt b/feature/favorite/src/main/java/com/squirtles/feature/favorite/FavoriteListViewModel.kt similarity index 63% rename from app/src/main/java/com/squirtles/musicroad/favorite/FavoriteListViewModel.kt rename to feature/favorite/src/main/java/com/squirtles/feature/favorite/FavoriteListViewModel.kt index cc69dcd6..9e32fb82 100644 --- a/app/src/main/java/com/squirtles/musicroad/favorite/FavoriteListViewModel.kt +++ b/feature/favorite/src/main/java/com/squirtles/feature/favorite/FavoriteListViewModel.kt @@ -1,11 +1,11 @@ -package com.squirtles.musicroad.favorite +package com.squirtles.feature.favorite -import com.squirtles.domain.usecase.favorite.DeleteFavoriteUseCase -import com.squirtles.domain.usecase.favorite.FetchFavoritePicksUseCase -import com.squirtles.domain.usecase.order.GetFavoriteListOrderUseCase -import com.squirtles.domain.usecase.order.SaveFavoriteListOrderUseCase -import com.squirtles.domain.usecase.user.GetCurrentUidUseCase -import com.squirtles.musicroad.common.picklist.PickListViewModel +import com.squirtles.domain.favorite.usecase.DeleteFavoriteUseCase +import com.squirtles.domain.order.usecase.GetFavoriteListOrderUseCase +import com.squirtles.domain.order.usecase.SaveFavoriteListOrderUseCase +import com.squirtles.domain.pick.usecase.FetchFavoritePicksUseCase +import com.squirtles.core.picklist.PickListViewModel +import com.squirtles.domain.user.usecase.GetCurrentUidUseCase import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject diff --git a/app/src/main/java/com/squirtles/musicroad/favorite/FavoriteScreen.kt b/feature/favorite/src/main/java/com/squirtles/feature/favorite/FavoriteScreen.kt similarity index 90% rename from app/src/main/java/com/squirtles/musicroad/favorite/FavoriteScreen.kt rename to feature/favorite/src/main/java/com/squirtles/feature/favorite/FavoriteScreen.kt index c2e9c2e6..a177efb7 100644 --- a/app/src/main/java/com/squirtles/musicroad/favorite/FavoriteScreen.kt +++ b/feature/favorite/src/main/java/com/squirtles/feature/favorite/FavoriteScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.favorite +package com.squirtles.feature.favorite import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -8,8 +8,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.squirtles.musicroad.common.picklist.PickListType -import com.squirtles.musicroad.common.picklist.PickListScreenContents +import com.squirtles.core.picklist.PickListScreenContents +import com.squirtles.core.picklist.PickListType @Composable fun FavoriteScreen( diff --git a/app/src/main/java/com/squirtles/musicroad/favorite/navigation/FavoriteNavigation.kt b/feature/favorite/src/main/java/com/squirtles/feature/favorite/navigation/FavoriteNavigation.kt similarity index 81% rename from app/src/main/java/com/squirtles/musicroad/favorite/navigation/FavoriteNavigation.kt rename to feature/favorite/src/main/java/com/squirtles/feature/favorite/navigation/FavoriteNavigation.kt index bc81d820..63b638cd 100644 --- a/app/src/main/java/com/squirtles/musicroad/favorite/navigation/FavoriteNavigation.kt +++ b/feature/favorite/src/main/java/com/squirtles/feature/favorite/navigation/FavoriteNavigation.kt @@ -1,12 +1,12 @@ -package com.squirtles.musicroad.favorite.navigation +package com.squirtles.feature.favorite.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.squirtles.musicroad.favorite.FavoriteScreen -import com.squirtles.musicroad.navigation.MainRoute +import com.squirtles.feature.favorite.FavoriteScreen +import com.squirtles.core.navigation.MainRoute fun NavController.navigateFavorite(uid: String, navOptions: NavOptions? = null) { navigate(MainRoute.Favorite(uid), navOptions) diff --git a/feature/favorite/src/test/java/com/squirtles/feature/favorite/ExampleUnitTest.kt b/feature/favorite/src/test/java/com/squirtles/feature/favorite/ExampleUnitTest.kt new file mode 100644 index 00000000..2e5010ef --- /dev/null +++ b/feature/favorite/src/test/java/com/squirtles/feature/favorite/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.squirtles.favorite + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/feature/main/.gitignore b/feature/main/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/main/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts new file mode 100644 index 00000000..d51337f7 --- /dev/null +++ b/feature/main/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(libs.plugins.musicroad.feature) + +} + +android { + namespace = "com.squirtles.feature.main" +} + +dependencies { + implementation(projects.feature.map) + implementation(projects.feature.create) + implementation(projects.feature.detail) + implementation(projects.feature.mypick) + implementation(projects.feature.favorite) + implementation(projects.feature.search) + implementation(projects.feature.userinfo) + implementation(projects.core.musicplayer) + implementation(projects.domain.user) + implementation(projects.domain.firebase) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.core.splashscreen) + implementation(libs.firebase.auth.ktx) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) +} diff --git a/feature/main/proguard-rules.pro b/feature/main/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/main/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/src/androidTest/java/com/squirtles/data/ExampleInstrumentedTest.kt b/feature/main/src/androidTest/java/com/squirtles/feature/main/ExampleInstrumentedTest.kt similarity index 86% rename from data/src/androidTest/java/com/squirtles/data/ExampleInstrumentedTest.kt rename to feature/main/src/androidTest/java/com/squirtles/feature/main/ExampleInstrumentedTest.kt index fe6ed515..92eae24b 100644 --- a/data/src/androidTest/java/com/squirtles/data/ExampleInstrumentedTest.kt +++ b/feature/main/src/androidTest/java/com/squirtles/feature/main/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package com.squirtles.data +package com.squirtles.main import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.squirtles.data.test", appContext.packageName) + assertEquals("com.squirtles.main.test", appContext.packageName) } -} \ No newline at end of file +} diff --git a/feature/main/src/main/AndroidManifest.xml b/feature/main/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d95426b4 --- /dev/null +++ b/feature/main/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/squirtles/musicroad/main/LoadingState.kt b/feature/main/src/main/java/com/squirtles/feature/main/LoadingState.kt similarity index 89% rename from app/src/main/java/com/squirtles/musicroad/main/LoadingState.kt rename to feature/main/src/main/java/com/squirtles/feature/main/LoadingState.kt index 0523c309..9b2a32fe 100644 --- a/app/src/main/java/com/squirtles/musicroad/main/LoadingState.kt +++ b/feature/main/src/main/java/com/squirtles/feature/main/LoadingState.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.main +package com.squirtles.feature.main sealed class LoadingState { data object Loading : LoadingState() diff --git a/app/src/main/java/com/squirtles/musicroad/main/MainActivity.kt b/feature/main/src/main/java/com/squirtles/feature/main/MainActivity.kt similarity index 93% rename from app/src/main/java/com/squirtles/musicroad/main/MainActivity.kt rename to feature/main/src/main/java/com/squirtles/feature/main/MainActivity.kt index 94d61eb7..c1fe6bb6 100644 --- a/app/src/main/java/com/squirtles/musicroad/main/MainActivity.kt +++ b/feature/main/src/main/java/com/squirtles/feature/main/MainActivity.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.main +package com.squirtles.feature.main import android.Manifest import android.content.Context @@ -22,13 +22,11 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.compose.rememberNavController import com.google.firebase.auth.FirebaseAuth -import com.squirtles.musicroad.R -import com.squirtles.musicroad.main.navigation.MainNavHost -import com.squirtles.musicroad.main.navigation.MainNavigator -import com.squirtles.musicroad.main.navigation.rememberMainNavigator -import com.squirtles.musicroad.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.feature.main.navigation.MainNavHost +import com.squirtles.feature.main.navigation.MainNavigator +import com.squirtles.feature.main.navigation.rememberMainNavigator import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.cancel import kotlinx.coroutines.launch @@ -141,10 +139,9 @@ class MainActivity : AppCompatActivity() { val navigator: MainNavigator = rememberMainNavigator() MusicRoadTheme { - val navController = rememberNavController() - MainNavHost( navigator = navigator, + finishActivity = { this.finish() }, ) } } diff --git a/app/src/main/java/com/squirtles/musicroad/main/MainViewModel.kt b/feature/main/src/main/java/com/squirtles/feature/main/MainViewModel.kt similarity index 88% rename from app/src/main/java/com/squirtles/musicroad/main/MainViewModel.kt rename to feature/main/src/main/java/com/squirtles/feature/main/MainViewModel.kt index 695d8700..650319bc 100644 --- a/app/src/main/java/com/squirtles/musicroad/main/MainViewModel.kt +++ b/feature/main/src/main/java/com/squirtles/feature/main/MainViewModel.kt @@ -1,11 +1,11 @@ -package com.squirtles.musicroad.main +package com.squirtles.feature.main import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.squirtles.domain.firebase.FirebaseException -import com.squirtles.domain.usecase.user.FetchUserByIdUseCase -import com.squirtles.domain.usecase.user.GetCurrentUidUseCase +import com.squirtles.domain.user.usecase.FetchUserByIdUseCase +import com.squirtles.domain.user.usecase.GetCurrentUidUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -48,7 +48,7 @@ class MainViewModel @Inject constructor( } .onFailure { exception -> when (exception) { - is FirebaseException.UserNotFoundException -> { + is FirebaseException.FetchDocumentFailedException -> { _loadingState.emit(LoadingState.UserNotFoundError(exception.message)) } diff --git a/app/src/main/java/com/squirtles/musicroad/main/NeedPermissionDialog.kt b/feature/main/src/main/java/com/squirtles/feature/main/NeedPermissionDialog.kt similarity index 88% rename from app/src/main/java/com/squirtles/musicroad/main/NeedPermissionDialog.kt rename to feature/main/src/main/java/com/squirtles/feature/main/NeedPermissionDialog.kt index a61c2119..0de66b78 100644 --- a/app/src/main/java/com/squirtles/musicroad/main/NeedPermissionDialog.kt +++ b/feature/main/src/main/java/com/squirtles/feature/main/NeedPermissionDialog.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.main +package com.squirtles.feature.main import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -21,13 +21,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.HorizontalSpacer -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.DarkGray -import com.squirtles.musicroad.ui.theme.MusicRoadTheme -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.HorizontalSpacer +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.DarkGray +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.White @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/squirtles/musicroad/main/PermissionBar.kt b/feature/main/src/main/java/com/squirtles/feature/main/PermissionBar.kt similarity index 95% rename from app/src/main/java/com/squirtles/musicroad/main/PermissionBar.kt rename to feature/main/src/main/java/com/squirtles/feature/main/PermissionBar.kt index 9f9f46ee..1dcf1c1d 100644 --- a/app/src/main/java/com/squirtles/musicroad/main/PermissionBar.kt +++ b/feature/main/src/main/java/com/squirtles/feature/main/PermissionBar.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.main +package com.squirtles.feature.main import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -21,8 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.Constants.DEFAULT_PADDING +import com.squirtles.core.common.ui.Constants.DEFAULT_PADDING @Composable fun PermissionBar( diff --git a/app/src/main/java/com/squirtles/musicroad/main/navigation/MainNavHost.kt b/feature/main/src/main/java/com/squirtles/feature/main/navigation/MainNavHost.kt similarity index 65% rename from app/src/main/java/com/squirtles/musicroad/main/navigation/MainNavHost.kt rename to feature/main/src/main/java/com/squirtles/feature/main/navigation/MainNavHost.kt index f546f96b..e25633b2 100644 --- a/app/src/main/java/com/squirtles/musicroad/main/navigation/MainNavHost.kt +++ b/feature/main/src/main/java/com/squirtles/feature/main/navigation/MainNavHost.kt @@ -1,20 +1,24 @@ -package com.squirtles.musicroad.main.navigation +package com.squirtles.feature.main.navigation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost -import com.squirtles.musicroad.favorite.navigation.favoriteNavGraph -import com.squirtles.musicroad.map.MapViewModel -import com.squirtles.musicroad.map.navigation.mapNavGraph -import com.squirtles.musicroad.media.PlayerServiceViewModel -import com.squirtles.musicroad.search.navigation.searchNavGraph -import com.squirtles.musicroad.userinfo.navigation.userInfoNavGraph +import com.squirtles.feature.create.navigation.createNavGraph +import com.squirtles.feature.detail.navigation.detailNavGraph +import com.squirtles.feature.favorite.navigation.favoriteNavGraph +import com.squirtles.feature.map.MapViewModel +import com.squirtles.feature.map.navigation.mapNavGraph +import com.squirtles.core.musicplayer.PlayerServiceViewModel +import com.squirtles.feature.mypick.navigation.myPickNavGraph +import com.squirtles.feature.search.navigation.searchNavGraph +import com.squirtles.feature.userinfo.navigation.userInfoNavGraph @Composable internal fun MainNavHost( modifier: Modifier = Modifier, navigator: MainNavigator, + finishActivity: () -> Unit, mapViewModel: MapViewModel = hiltViewModel(), playerServiceViewModel: PlayerServiceViewModel = hiltViewModel(), ) { @@ -29,13 +33,23 @@ internal fun MainNavHost( onCenterClick = navigator::navigateSearch, onUserInfoClick = navigator::navigateUserInfo, onPickSummaryClick = navigator::navigatePickDetail, - onBackClick = navigator::popBackStackIfNotMap, - onDeleted = mapViewModel::resetClickedMarkerState + onLoadingDialogCloseClick = finishActivity ) searchNavGraph( onBackClick = navigator::popBackStackIfNotMap, onItemClick = navigator::navigateCreate, + ) + + detailNavGraph( + playerServiceViewModel = playerServiceViewModel, + onUserInfoClick = navigator::navigateUserInfo, + onBackClick = navigator::popBackStackIfNotMap, + onDeleted = mapViewModel::resetClickedMarkerState + ) + + createNavGraph( + onBackClick = navigator::popBackStackIfNotMap, onCreateClick = { pickId -> navigator.navigatePickDetail(pickId, true) }, @@ -48,12 +62,16 @@ internal fun MainNavHost( userInfoNavGraph( onBackClick = navigator::popBackStackIfNotMap, - onItemClick = navigator::navigatePickDetail, onBackToMapClick = navigator::navigateMap, onFavoritePicksClick = navigator::navigateFavorite, onMyPicksClick = navigator::navigateMyPicks, onEditProfileClick = navigator::navigateEditProfile, onEditNotificationClick = navigator::navigateEditNotificationSetting, ) + + myPickNavGraph( + onBackClick = navigator::popBackStackIfNotMap, + onItemClick = navigator::navigatePickDetail, + ) } } diff --git a/app/src/main/java/com/squirtles/musicroad/main/navigation/MainNavigator.kt b/feature/main/src/main/java/com/squirtles/feature/main/navigation/MainNavigator.kt similarity index 81% rename from app/src/main/java/com/squirtles/musicroad/main/navigation/MainNavigator.kt rename to feature/main/src/main/java/com/squirtles/feature/main/navigation/MainNavigator.kt index e07b5442..88f605d1 100644 --- a/app/src/main/java/com/squirtles/musicroad/main/navigation/MainNavigator.kt +++ b/feature/main/src/main/java/com/squirtles/feature/main/navigation/MainNavigator.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.main.navigation +package com.squirtles.feature.main.navigation import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -8,17 +8,17 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.favorite.navigation.navigateFavorite -import com.squirtles.musicroad.map.navigation.navigateMap -import com.squirtles.musicroad.map.navigation.navigatePickDetail -import com.squirtles.musicroad.navigation.Route -import com.squirtles.musicroad.search.navigation.navigateCreate -import com.squirtles.musicroad.search.navigation.navigateSearch -import com.squirtles.musicroad.userinfo.navigation.navigateEditNotificationSetting -import com.squirtles.musicroad.userinfo.navigation.navigateEditProfile -import com.squirtles.musicroad.userinfo.navigation.navigateMyPicks -import com.squirtles.musicroad.userinfo.navigation.navigateUserInfo +import com.squirtles.feature.create.navigation.navigateCreate +import com.squirtles.feature.detail.navigation.navigatePickDetail +import com.squirtles.feature.favorite.navigation.navigateFavorite +import com.squirtles.feature.map.navigation.navigateMap +import com.squirtles.core.model.Song +import com.squirtles.feature.mypick.navigation.navigateMyPicks +import com.squirtles.core.navigation.Route +import com.squirtles.feature.search.navigation.navigateSearch +import com.squirtles.feature.userinfo.navigation.navigateEditNotificationSetting +import com.squirtles.feature.userinfo.navigation.navigateEditProfile +import com.squirtles.feature.userinfo.navigation.navigateUserInfo internal class MainNavigator( val navController: NavHostController diff --git a/app/src/main/res/values/colors.xml b/feature/main/src/main/res/values/colors.xml similarity index 100% rename from app/src/main/res/values/colors.xml rename to feature/main/src/main/res/values/colors.xml diff --git a/feature/main/src/main/res/values/strings.xml b/feature/main/src/main/res/values/strings.xml new file mode 100644 index 00000000..2c403a91 --- /dev/null +++ b/feature/main/src/main/res/values/strings.xml @@ -0,0 +1,15 @@ + + + 확인 + 권한 요청 + 앱 실행을 위해\n위치와 마이크 권한이 필요합니다. + + + 설정(앱 정보)에서 권한을 허용해주세요. + 설정으로 이동 + + + 네트워크 연결이 원활하지 않습니다. + 유저 정보가 존재하지 않습니다. 앱을 재설치 해주세요. + 유저 등록 실패. 문의해주세요 + diff --git a/app/src/main/res/values/themes.xml b/feature/main/src/main/res/values/themes.xml similarity index 100% rename from app/src/main/res/values/themes.xml rename to feature/main/src/main/res/values/themes.xml diff --git a/domain/src/test/java/com/squirtles/domain/ExampleUnitTest.kt b/feature/main/src/test/java/com/squirtles/feature/main/ExampleUnitTest.kt similarity index 91% rename from domain/src/test/java/com/squirtles/domain/ExampleUnitTest.kt rename to feature/main/src/test/java/com/squirtles/feature/main/ExampleUnitTest.kt index 16fc0af5..10ddfe44 100644 --- a/domain/src/test/java/com/squirtles/domain/ExampleUnitTest.kt +++ b/feature/main/src/test/java/com/squirtles/feature/main/ExampleUnitTest.kt @@ -1,4 +1,4 @@ -package com.squirtles.domain +package com.squirtles.main import org.junit.Test @@ -14,4 +14,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/feature/map/.gitignore b/feature/map/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/map/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts new file mode 100644 index 00000000..997c8d89 --- /dev/null +++ b/feature/map/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(libs.plugins.musicroad.feature) +} + +android { + namespace = "com.squirtles.feature.map" +} + +dependencies { + implementation(projects.core.account) + implementation(projects.core.musicplayer) + implementation(projects.domain.pick) + implementation(projects.domain.location) + implementation(projects.domain.user) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) + + // Map + implementation(libs.map.sdk) + implementation(libs.play.services.location) + implementation(libs.bundles.coil) + implementation(libs.bundles.auth) +} diff --git a/feature/map/proguard-rules.pro b/feature/map/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/map/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/map/src/androidTest/java/com/squirtles/feature/map/ExampleInstrumentedTest.kt b/feature/map/src/androidTest/java/com/squirtles/feature/map/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..0b3933fe --- /dev/null +++ b/feature/map/src/androidTest/java/com/squirtles/feature/map/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.squirtles.map + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.squirtles.map.test", appContext.packageName) + } +} diff --git a/feature/map/src/main/AndroidManifest.xml b/feature/map/src/main/AndroidManifest.xml new file mode 100644 index 00000000..706c2553 --- /dev/null +++ b/feature/map/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/src/main/java/com/squirtles/musicroad/map/Constants.kt b/feature/map/src/main/java/com/squirtles/feature/map/Constants.kt similarity index 86% rename from app/src/main/java/com/squirtles/musicroad/map/Constants.kt rename to feature/map/src/main/java/com/squirtles/feature/map/Constants.kt index 92fad232..3e1a202c 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/Constants.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/Constants.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map +package com.squirtles.feature.map internal enum class BottomNavigationSize( val size: Int diff --git a/app/src/main/java/com/squirtles/musicroad/map/MapScreen.kt b/feature/map/src/main/java/com/squirtles/feature/map/MapScreen.kt similarity index 93% rename from app/src/main/java/com/squirtles/musicroad/map/MapScreen.kt rename to feature/map/src/main/java/com/squirtles/feature/map/MapScreen.kt index 2228f944..decd1c38 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/MapScreen.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/MapScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map +package com.squirtles.feature.map import android.widget.Toast import androidx.activity.compose.BackHandler @@ -34,19 +34,17 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.squirtles.musicroad.R -import com.squirtles.musicroad.account.AccountViewModel -import com.squirtles.musicroad.account.GoogleId -import com.squirtles.musicroad.common.SignInAlertDialog -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.main.MainActivity -import com.squirtles.musicroad.map.components.ClusterBottomSheet -import com.squirtles.musicroad.map.components.InfoWindow -import com.squirtles.musicroad.map.components.LoadingDialog -import com.squirtles.musicroad.map.components.MapBottomNavBar -import com.squirtles.musicroad.map.components.PickNotificationBanner -import com.squirtles.musicroad.media.PlayerServiceViewModel -import com.squirtles.musicroad.ui.theme.Black +import com.squirtles.core.account.AccountViewModel +import com.squirtles.core.account.GoogleId +import com.squirtles.core.common.ui.SignInAlertDialog +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.musicplayer.PlayerServiceViewModel +import com.squirtles.feature.map.components.ClusterBottomSheet +import com.squirtles.feature.map.components.InfoWindow +import com.squirtles.feature.map.components.LoadingDialog +import com.squirtles.feature.map.components.MapBottomNavBar +import com.squirtles.feature.map.components.PickNotificationBanner import kotlinx.coroutines.launch @Composable @@ -57,6 +55,7 @@ fun MapScreen( onCenterClick: () -> Unit, onUserInfoClick: (String) -> Unit, onPickSummaryClick: (String) -> Unit, + onLoadingDialogCloseClick: () -> Unit, accountViewModel: AccountViewModel = hiltViewModel() ) { val nearPicks by mapViewModel.nearPicks.collectAsStateWithLifecycle() @@ -250,7 +249,7 @@ fun MapScreen( ) { LoadingDialog( onCloseClick = { - (context as MainActivity).finish() + onLoadingDialogCloseClick() } ) } diff --git a/app/src/main/java/com/squirtles/musicroad/map/MapViewModel.kt b/feature/map/src/main/java/com/squirtles/feature/map/MapViewModel.kt similarity index 95% rename from app/src/main/java/com/squirtles/musicroad/map/MapViewModel.kt rename to feature/map/src/main/java/com/squirtles/feature/map/MapViewModel.kt index 7f79eb05..02ea7e62 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/MapViewModel.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/MapViewModel.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map +package com.squirtles.feature.map import android.content.Context import android.location.Location @@ -8,12 +8,12 @@ import com.naver.maps.geometry.LatLng import com.naver.maps.map.CameraPosition import com.naver.maps.map.clustering.Clusterer import com.naver.maps.map.overlay.Marker -import com.squirtles.domain.model.Pick -import com.squirtles.domain.usecase.location.GetLastLocationUseCase -import com.squirtles.domain.usecase.location.SaveLastLocationUseCase -import com.squirtles.domain.usecase.pick.FetchPickUseCase -import com.squirtles.domain.usecase.user.GetCurrentUidUseCase -import com.squirtles.musicroad.map.marker.MarkerKey +import com.squirtles.domain.location.usecase.GetLastLocationUseCase +import com.squirtles.domain.location.usecase.SaveLastLocationUseCase +import com.squirtles.feature.map.marker.MarkerKey +import com.squirtles.core.model.Pick +import com.squirtles.domain.pick.usecase.FetchPickUseCase +import com.squirtles.domain.user.usecase.GetCurrentUidUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/squirtles/musicroad/map/NaverMap.kt b/feature/map/src/main/java/com/squirtles/feature/map/NaverMap.kt similarity index 97% rename from app/src/main/java/com/squirtles/musicroad/map/NaverMap.kt rename to feature/map/src/main/java/com/squirtles/feature/map/NaverMap.kt index 26f7b17c..5f1097c9 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/NaverMap.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/NaverMap.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map +package com.squirtles.feature.map import android.Manifest import android.app.Activity @@ -41,11 +41,10 @@ import com.naver.maps.map.overlay.CircleOverlay import com.naver.maps.map.overlay.LocationOverlay import com.naver.maps.map.overlay.OverlayImage import com.naver.maps.map.util.FusedLocationSource -import com.squirtles.musicroad.R -import com.squirtles.musicroad.map.marker.MarkerKey -import com.squirtles.musicroad.map.marker.buildClusterer -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.Purple15 +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.Purple15 +import com.squirtles.feature.map.marker.MarkerKey +import com.squirtles.feature.map.marker.buildClusterer import kotlinx.coroutines.launch import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine diff --git a/app/src/main/java/com/squirtles/musicroad/map/components/ClusterBottomSheet.kt b/feature/map/src/main/java/com/squirtles/feature/map/components/ClusterBottomSheet.kt similarity index 85% rename from app/src/main/java/com/squirtles/musicroad/map/components/ClusterBottomSheet.kt rename to feature/map/src/main/java/com/squirtles/feature/map/components/ClusterBottomSheet.kt index 9d0e5c08..38abeffd 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/components/ClusterBottomSheet.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/components/ClusterBottomSheet.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map.components +package com.squirtles.feature.map.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -21,20 +21,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -import com.squirtles.domain.model.LocationPoint -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.common.AlbumImage -import com.squirtles.musicroad.common.CommentText -import com.squirtles.musicroad.common.Constants.DEFAULT_PADDING -import com.squirtles.musicroad.common.Constants.REQUEST_IMAGE_SIZE_DEFAULT -import com.squirtles.musicroad.common.CountText -import com.squirtles.musicroad.common.CreatedByOtherUserText -import com.squirtles.musicroad.common.CreatedBySelfText -import com.squirtles.musicroad.common.FavoriteCountText -import com.squirtles.musicroad.common.HorizontalSpacer -import com.squirtles.musicroad.common.SongInfoText -import com.squirtles.musicroad.common.VerticalSpacer +import com.squirtles.core.common.ui.AlbumImage +import com.squirtles.core.common.ui.CommentText +import com.squirtles.core.common.ui.Constants.DEFAULT_PADDING +import com.squirtles.core.common.ui.Constants.REQUEST_IMAGE_SIZE_DEFAULT +import com.squirtles.core.common.ui.CountText +import com.squirtles.core.common.ui.CreatedByOtherUserText +import com.squirtles.core.common.ui.CreatedBySelfText +import com.squirtles.core.common.ui.FavoriteCountText +import com.squirtles.core.common.ui.HorizontalSpacer +import com.squirtles.core.common.ui.SongInfoText +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.model.LocationPoint +import com.squirtles.core.model.Pick +import com.squirtles.core.model.Song import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -63,7 +63,7 @@ fun ClusterBottomSheet( .fillMaxWidth() .padding(start = DEFAULT_PADDING) ) - + VerticalSpacer(height = 8) LazyColumn { diff --git a/app/src/main/java/com/squirtles/musicroad/map/components/InfoWindowCard.kt b/feature/map/src/main/java/com/squirtles/feature/map/components/InfoWindowCard.kt similarity index 88% rename from app/src/main/java/com/squirtles/musicroad/map/components/InfoWindowCard.kt rename to feature/map/src/main/java/com/squirtles/feature/map/components/InfoWindowCard.kt index f83525aa..86796c1f 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/components/InfoWindowCard.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/components/InfoWindowCard.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map.components +package com.squirtles.feature.map.components import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.util.Size @@ -24,18 +24,18 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt -import com.squirtles.domain.model.Creator -import com.squirtles.domain.model.LocationPoint -import com.squirtles.domain.model.Pick -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.common.AlbumImage -import com.squirtles.musicroad.common.CreatedByOtherUserText -import com.squirtles.musicroad.common.CreatedBySelfText -import com.squirtles.musicroad.common.FavoriteCountText -import com.squirtles.musicroad.common.HorizontalSpacer -import com.squirtles.musicroad.common.SongInfoText -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.AlbumImage +import com.squirtles.core.common.ui.CreatedByOtherUserText +import com.squirtles.core.common.ui.CreatedBySelfText +import com.squirtles.core.common.ui.FavoriteCountText +import com.squirtles.core.common.ui.HorizontalSpacer +import com.squirtles.core.common.ui.SongInfoText +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.model.Creator +import com.squirtles.core.model.LocationPoint +import com.squirtles.core.model.Pick +import com.squirtles.core.model.Song @Composable fun InfoWindow( diff --git a/app/src/main/java/com/squirtles/musicroad/map/components/LoadingDialog.kt b/feature/map/src/main/java/com/squirtles/feature/map/components/LoadingDialog.kt similarity index 86% rename from app/src/main/java/com/squirtles/musicroad/map/components/LoadingDialog.kt rename to feature/map/src/main/java/com/squirtles/feature/map/components/LoadingDialog.kt index 7eda5d40..314b7752 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/components/LoadingDialog.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/components/LoadingDialog.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map.components +package com.squirtles.feature.map.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,13 +20,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.DarkGray -import com.squirtles.musicroad.ui.theme.MusicRoadTheme -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.DarkGray +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.map.R @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/squirtles/musicroad/map/components/MapBottomNavBar.kt b/feature/map/src/main/java/com/squirtles/feature/map/components/MapBottomNavBar.kt similarity index 92% rename from app/src/main/java/com/squirtles/musicroad/map/components/MapBottomNavBar.kt rename to feature/map/src/main/java/com/squirtles/feature/map/components/MapBottomNavBar.kt index e50daaab..8de63566 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/components/MapBottomNavBar.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/components/MapBottomNavBar.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map.components +package com.squirtles.feature.map.components import android.content.res.Configuration import android.location.Location @@ -23,12 +23,12 @@ 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 com.squirtles.musicroad.R -import com.squirtles.musicroad.map.BottomNavigationIconSize -import com.squirtles.musicroad.map.BottomNavigationSize -import com.squirtles.musicroad.map.navigation.NavTab -import com.squirtles.musicroad.ui.theme.MusicRoadTheme -import com.squirtles.musicroad.ui.theme.Primary +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.feature.map.BottomNavigationSize +import com.squirtles.feature.map.BottomNavigationIconSize +import com.squirtles.feature.map.R +import com.squirtles.feature.map.navigation.NavTab @Composable internal fun MapBottomNavBar( diff --git a/app/src/main/java/com/squirtles/musicroad/map/components/PickNotificationBanner.kt b/feature/map/src/main/java/com/squirtles/feature/map/components/PickNotificationBanner.kt similarity index 69% rename from app/src/main/java/com/squirtles/musicroad/map/components/PickNotificationBanner.kt rename to feature/map/src/main/java/com/squirtles/feature/map/components/PickNotificationBanner.kt index ed665a9d..80c8ed0b 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/components/PickNotificationBanner.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/components/PickNotificationBanner.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map.components +package com.squirtles.feature.map.components import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.RepeatMode @@ -23,12 +23,15 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squirtles.domain.model.Pick -import com.squirtles.musicroad.R -import com.squirtles.musicroad.detail.DetailViewModel.Companion.DEFAULT_PICK -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.MusicRoadTheme -import com.squirtles.musicroad.ui.theme.White +import androidx.core.graphics.toColorInt +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.model.Creator +import com.squirtles.core.model.LocationPoint +import com.squirtles.core.model.Pick +import com.squirtles.core.model.Song +import com.squirtles.feature.map.R @Composable fun PickNotificationBanner( @@ -77,7 +80,27 @@ fun PickNotificationBanner( private fun PickNotificationBannerPreview() { MusicRoadTheme { PickNotificationBanner( - nearPicks = listOf(DEFAULT_PICK), + nearPicks = listOf( Pick( + id = "", + song = Song( + id = "", + songName = "", + artistName = "", + albumName = "", + imageUrl = "", + genreNames = listOf(), + bgColor = "#000000".toColorInt(), + externalUrl = "", + previewUrl = "" + ), + comment = "", + createdAt = "", + createdBy = Creator(uid = "", userName = "짱구"), + favoriteCount = 0, + location = LocationPoint(1.0, 1.0), + musicVideoUrl = "", + ) + ), onClick = { } ) } diff --git a/app/src/main/java/com/squirtles/musicroad/map/marker/ClusterMarkerIconView.kt b/feature/map/src/main/java/com/squirtles/feature/map/marker/ClusterMarkerIconView.kt similarity index 97% rename from app/src/main/java/com/squirtles/musicroad/map/marker/ClusterMarkerIconView.kt rename to feature/map/src/main/java/com/squirtles/feature/map/marker/ClusterMarkerIconView.kt index 7603c49d..0601ff39 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/marker/ClusterMarkerIconView.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/marker/ClusterMarkerIconView.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map.marker +package com.squirtles.feature.map.marker import android.annotation.SuppressLint import android.content.Context diff --git a/app/src/main/java/com/squirtles/musicroad/map/marker/Clusterer.kt b/feature/map/src/main/java/com/squirtles/feature/map/marker/Clusterer.kt similarity index 90% rename from app/src/main/java/com/squirtles/musicroad/map/marker/Clusterer.kt rename to feature/map/src/main/java/com/squirtles/feature/map/marker/Clusterer.kt index 51555035..eb4d14cb 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/marker/Clusterer.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/marker/Clusterer.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map.marker +package com.squirtles.feature.map.marker import android.content.Context import android.graphics.PointF @@ -15,13 +15,14 @@ import com.naver.maps.map.overlay.Align import com.naver.maps.map.overlay.Marker import com.naver.maps.map.overlay.Overlay import com.naver.maps.map.overlay.OverlayImage -import com.squirtles.musicroad.map.DEFAULT_MARKER_Z_INDEX -import com.squirtles.musicroad.map.MapViewModel -import com.squirtles.musicroad.map.setCameraToMarker -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.Blue -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.Constants.REQUEST_IMAGE_SIZE_DEFAULT +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.Blue +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.map.DEFAULT_MARKER_Z_INDEX +import com.squirtles.feature.map.MapViewModel +import com.squirtles.feature.map.setCameraToMarker internal fun buildClusterer( context: Context, @@ -126,7 +127,12 @@ internal fun buildClusterer( val color = if (pick.createdBy.uid == mapViewModel.getUid()) Blue else Primary setPaintColor(color.toArgb()) } - leafMarkerIconView.setLeafMarkerIcon(pick) { + leafMarkerIconView.setLeafMarkerIcon( + pick.song.getImageUrlWithSize( + REQUEST_IMAGE_SIZE_DEFAULT.width, + REQUEST_IMAGE_SIZE_DEFAULT.height + ) + ) { marker.icon = OverlayImage.fromView(leafMarkerIconView) marker.setOnClickListener { marker.map?.let { map -> diff --git a/app/src/main/java/com/squirtles/musicroad/map/marker/DensityType.kt b/feature/map/src/main/java/com/squirtles/feature/map/marker/DensityType.kt similarity index 50% rename from app/src/main/java/com/squirtles/musicroad/map/marker/DensityType.kt rename to feature/map/src/main/java/com/squirtles/feature/map/marker/DensityType.kt index eea0e7f6..315902a8 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/marker/DensityType.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/marker/DensityType.kt @@ -1,9 +1,9 @@ -package com.squirtles.musicroad.map.marker +package com.squirtles.feature.map.marker import androidx.compose.ui.graphics.toArgb -import com.squirtles.musicroad.ui.theme.Primary20 -import com.squirtles.musicroad.ui.theme.Primary50 -import com.squirtles.musicroad.ui.theme.Primary80 +import com.squirtles.core.common.ui.theme.Primary20 +import com.squirtles.core.common.ui.theme.Primary50 +import com.squirtles.core.common.ui.theme.Primary80 enum class DensityType(val offset: Int, val color: Int) { LOW(4, Primary80.toArgb()), diff --git a/app/src/main/java/com/squirtles/musicroad/map/marker/LeafMarkerIconView.kt b/feature/map/src/main/java/com/squirtles/feature/map/marker/LeafMarkerIconView.kt similarity index 87% rename from app/src/main/java/com/squirtles/musicroad/map/marker/LeafMarkerIconView.kt rename to feature/map/src/main/java/com/squirtles/feature/map/marker/LeafMarkerIconView.kt index 159f19a0..521aee18 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/marker/LeafMarkerIconView.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/marker/LeafMarkerIconView.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map.marker +package com.squirtles.feature.map.marker import android.content.Context import android.graphics.Bitmap @@ -14,8 +14,6 @@ import coil3.request.allowHardware import coil3.request.transformations import coil3.toBitmap import coil3.transform.CircleCropTransformation -import com.squirtles.domain.model.Pick -import com.squirtles.musicroad.common.Constants.REQUEST_IMAGE_SIZE_DEFAULT class LeafMarkerIconView( context: Context, @@ -70,9 +68,8 @@ class LeafMarkerIconView( strokePaint.color = color } - fun setLeafMarkerIcon(pick: Pick, onImageLoaded: () -> Unit) { - val song = pick.song - loadImage(song.getImageUrlWithSize(REQUEST_IMAGE_SIZE_DEFAULT.width, REQUEST_IMAGE_SIZE_DEFAULT.height)) { + fun setLeafMarkerIcon(imgUrl: String?, onImageLoaded: () -> Unit) { + loadImage(imgUrl) { onImageLoaded() } } @@ -87,7 +84,7 @@ class LeafMarkerIconView( bitmap = result.image.toBitmap() onImageLoaded() }, - onError = { _, error -> + onError = { _, _ -> onImageLoaded() } ) diff --git a/app/src/main/java/com/squirtles/musicroad/map/marker/MarkerKey.kt b/feature/map/src/main/java/com/squirtles/feature/map/marker/MarkerKey.kt similarity index 74% rename from app/src/main/java/com/squirtles/musicroad/map/marker/MarkerKey.kt rename to feature/map/src/main/java/com/squirtles/feature/map/marker/MarkerKey.kt index 41b308a3..4c679880 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/marker/MarkerKey.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/marker/MarkerKey.kt @@ -1,8 +1,8 @@ -package com.squirtles.musicroad.map.marker +package com.squirtles.feature.map.marker import com.naver.maps.geometry.LatLng import com.naver.maps.map.clustering.ClusteringKey -import com.squirtles.domain.model.Pick +import com.squirtles.core.model.Pick data class MarkerKey(val pick: Pick) : ClusteringKey { override fun getPosition() = LatLng(pick.location.latitude, pick.location.longitude) diff --git a/feature/map/src/main/java/com/squirtles/feature/map/navigation/MapNavigation.kt b/feature/map/src/main/java/com/squirtles/feature/map/navigation/MapNavigation.kt new file mode 100644 index 00000000..20fb13cd --- /dev/null +++ b/feature/map/src/main/java/com/squirtles/feature/map/navigation/MapNavigation.kt @@ -0,0 +1,36 @@ +package com.squirtles.feature.map.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.squirtles.feature.map.MapScreen +import com.squirtles.feature.map.MapViewModel +import com.squirtles.core.musicplayer.PlayerServiceViewModel +import com.squirtles.core.navigation.Route + +fun NavController.navigateMap(navOptions: NavOptions? = null) { + navigate(Route.Map, navOptions) +} + +fun NavGraphBuilder.mapNavGraph( + mapViewModel: MapViewModel, + playerServiceViewModel: PlayerServiceViewModel, + onFavoriteClick: (String) -> Unit, + onCenterClick: () -> Unit, + onUserInfoClick: (String) -> Unit, + onPickSummaryClick: (String) -> Unit, + onLoadingDialogCloseClick: () -> Unit +) { + composable { + MapScreen( + mapViewModel = mapViewModel, + playerServiceViewModel = playerServiceViewModel, + onFavoriteClick = onFavoriteClick, + onCenterClick = onCenterClick, + onUserInfoClick = onUserInfoClick, + onPickSummaryClick = onPickSummaryClick, + onLoadingDialogCloseClick = onLoadingDialogCloseClick + ) + } +} diff --git a/app/src/main/java/com/squirtles/musicroad/map/navigation/NavTab.kt b/feature/map/src/main/java/com/squirtles/feature/map/navigation/NavTab.kt similarity index 87% rename from app/src/main/java/com/squirtles/musicroad/map/navigation/NavTab.kt rename to feature/map/src/main/java/com/squirtles/feature/map/navigation/NavTab.kt index d24b0c21..b1f51001 100644 --- a/app/src/main/java/com/squirtles/musicroad/map/navigation/NavTab.kt +++ b/feature/map/src/main/java/com/squirtles/feature/map/navigation/NavTab.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.map.navigation +package com.squirtles.feature.map.navigation import androidx.annotation.StringRes import androidx.compose.material.icons.Icons @@ -6,8 +6,8 @@ import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.MusicNote import androidx.compose.ui.graphics.vector.ImageVector -import com.squirtles.musicroad.R -import com.squirtles.musicroad.map.BottomNavigationIconSize +import com.squirtles.feature.map.BottomNavigationIconSize +import com.squirtles.feature.map.R internal enum class NavTab( @StringRes val contentDescription: Int, diff --git a/app/src/main/res/drawable/ic_location.xml b/feature/map/src/main/res/drawable/ic_location.xml similarity index 100% rename from app/src/main/res/drawable/ic_location.xml rename to feature/map/src/main/res/drawable/ic_location.xml diff --git a/app/src/main/res/drawable/ic_musical_note_64.png b/feature/map/src/main/res/drawable/ic_musical_note_64.png similarity index 100% rename from app/src/main/res/drawable/ic_musical_note_64.png rename to feature/map/src/main/res/drawable/ic_musical_note_64.png diff --git a/feature/map/src/main/res/values/strings.xml b/feature/map/src/main/res/values/strings.xml new file mode 100644 index 00000000..9749595a --- /dev/null +++ b/feature/map/src/main/res/values/strings.xml @@ -0,0 +1,23 @@ + + + 현재 위치 로딩 중... + 종료 + 데이터를 불러오는 데 일시적인 오류가 발생했습니다. + + + 픽 보관함 이동 버튼 아이콘 + 설정 이동 버튼 아이콘 + 내비게이션 중앙 버튼 아이콘 + 🎧 주변에 %d개의 픽이 있습니다! + 🔇 주변에 %d개의 픽이 있습니다! + 앨범 이미지 + 님의 픽 + 픽을 담은 개수 + 내가 + 등록한 픽 + + + 담은 픽을 확인하기 위해\n로그인이 필요합니다 + 픽을 등록하기 위해\n로그인이 필요합니다 + 로그인이 필요합니다 + diff --git a/data/src/test/java/com/squirtles/data/ExampleUnitTest.kt b/feature/map/src/test/java/com/squirtles/feature/map/ExampleUnitTest.kt similarity index 91% rename from data/src/test/java/com/squirtles/data/ExampleUnitTest.kt rename to feature/map/src/test/java/com/squirtles/feature/map/ExampleUnitTest.kt index 8e88260a..99fcf0f5 100644 --- a/data/src/test/java/com/squirtles/data/ExampleUnitTest.kt +++ b/feature/map/src/test/java/com/squirtles/feature/map/ExampleUnitTest.kt @@ -1,4 +1,4 @@ -package com.squirtles.data +package com.squirtles.map import org.junit.Test @@ -14,4 +14,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/feature/mypick/.gitignore b/feature/mypick/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/mypick/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/mypick/build.gradle.kts b/feature/mypick/build.gradle.kts new file mode 100644 index 00000000..ef7b19f8 --- /dev/null +++ b/feature/mypick/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.musicroad.feature) +} + +android { + namespace = "com.squirtles.feature.mypick" +} + +dependencies { + implementation(projects.core.picklist) + implementation(projects.domain.picklist) + implementation(projects.domain.pick) + implementation(projects.domain.order) + implementation(projects.domain.user) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) +} diff --git a/feature/mypick/proguard-rules.pro b/feature/mypick/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/mypick/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/mypick/src/androidTest/java/com/squirtles/feature/mypick/ExampleInstrumentedTest.kt b/feature/mypick/src/androidTest/java/com/squirtles/feature/mypick/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..dec8f396 --- /dev/null +++ b/feature/mypick/src/androidTest/java/com/squirtles/feature/mypick/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.mypick + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.mypick.test", appContext.packageName) + } +} diff --git a/app/src/main/java/com/squirtles/musicroad/mypick/MyPickListViewModel.kt b/feature/mypick/src/main/java/com/squirtles/feature/mypick/MyPickListViewModel.kt similarity index 62% rename from app/src/main/java/com/squirtles/musicroad/mypick/MyPickListViewModel.kt rename to feature/mypick/src/main/java/com/squirtles/feature/mypick/MyPickListViewModel.kt index 9a90d21d..d9ccb756 100644 --- a/app/src/main/java/com/squirtles/musicroad/mypick/MyPickListViewModel.kt +++ b/feature/mypick/src/main/java/com/squirtles/feature/mypick/MyPickListViewModel.kt @@ -1,11 +1,11 @@ -package com.squirtles.musicroad.mypick +package com.squirtles.feature.mypick -import com.squirtles.domain.usecase.mypick.DeletePickUseCase -import com.squirtles.domain.usecase.mypick.FetchMyPicksUseCase -import com.squirtles.domain.usecase.order.GetMyPickListOrderUseCase -import com.squirtles.domain.usecase.order.SaveMyPickListOrderUseCase -import com.squirtles.domain.usecase.user.GetCurrentUidUseCase -import com.squirtles.musicroad.common.picklist.PickListViewModel +import com.squirtles.domain.order.usecase.GetMyPickListOrderUseCase +import com.squirtles.domain.order.usecase.SaveMyPickListOrderUseCase +import com.squirtles.domain.pick.usecase.DeletePickUseCase +import com.squirtles.domain.pick.usecase.FetchMyPicksUseCase +import com.squirtles.core.picklist.PickListViewModel +import com.squirtles.domain.user.usecase.GetCurrentUidUseCase import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject diff --git a/app/src/main/java/com/squirtles/musicroad/mypick/MyPickScreen.kt b/feature/mypick/src/main/java/com/squirtles/feature/mypick/MyPickScreen.kt similarity index 90% rename from app/src/main/java/com/squirtles/musicroad/mypick/MyPickScreen.kt rename to feature/mypick/src/main/java/com/squirtles/feature/mypick/MyPickScreen.kt index c1ecbab2..acdd6f3b 100644 --- a/app/src/main/java/com/squirtles/musicroad/mypick/MyPickScreen.kt +++ b/feature/mypick/src/main/java/com/squirtles/feature/mypick/MyPickScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.mypick +package com.squirtles.feature.mypick import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -8,8 +8,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.squirtles.musicroad.common.picklist.PickListScreenContents -import com.squirtles.musicroad.common.picklist.PickListType +import com.squirtles.core.picklist.PickListScreenContents +import com.squirtles.core.picklist.PickListType @Composable fun MyPickScreen( diff --git a/feature/mypick/src/main/java/com/squirtles/feature/mypick/navigation/MyPickNavigation.kt b/feature/mypick/src/main/java/com/squirtles/feature/mypick/navigation/MyPickNavigation.kt new file mode 100644 index 00000000..7a9e0db3 --- /dev/null +++ b/feature/mypick/src/main/java/com/squirtles/feature/mypick/navigation/MyPickNavigation.kt @@ -0,0 +1,28 @@ +package com.squirtles.feature.mypick.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.squirtles.feature.mypick.MyPickScreen +import com.squirtles.core.navigation.UserInfoRoute + +fun NavController.navigateMyPicks(uid: String, navOptions: NavOptions) { + navigate(UserInfoRoute.MyPicks(uid), navOptions) +} + +fun NavGraphBuilder.myPickNavGraph( + onBackClick: () -> Unit, + onItemClick: (String) -> Unit, +) { + composable { backStackEntry -> + val uid = backStackEntry.toRoute().uid + + MyPickScreen( + uid = uid, + onBackClick = onBackClick, + onItemClick = onItemClick + ) + } +} diff --git a/feature/mypick/src/test/java/com/squirtles/feature/mypick/ExampleUnitTest.kt b/feature/mypick/src/test/java/com/squirtles/feature/mypick/ExampleUnitTest.kt new file mode 100644 index 00000000..7dec7030 --- /dev/null +++ b/feature/mypick/src/test/java/com/squirtles/feature/mypick/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.example.mypick + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/feature/search/.gitignore b/feature/search/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts new file mode 100644 index 00000000..ce8478d0 --- /dev/null +++ b/feature/search/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.musicroad.feature) +} + +android { + namespace = "com.squirtles.feature.search" +} + +dependencies { + implementation(projects.domain.applemusic) + + implementation(libs.androidx.paging.compose) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) +} diff --git a/feature/search/proguard-rules.pro b/feature/search/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/search/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/search/src/androidTest/java/com/squirtles/feature/search/ExampleInstrumentedTest.kt b/feature/search/src/androidTest/java/com/squirtles/feature/search/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..cbce775e --- /dev/null +++ b/feature/search/src/androidTest/java/com/squirtles/feature/search/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.search + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.search.test", appContext.packageName) + } +} diff --git a/app/src/main/java/com/squirtles/musicroad/search/SearchMusicScreen.kt b/feature/search/src/main/java/com/squirtles/feature/search/SearchMusicScreen.kt similarity index 93% rename from app/src/main/java/com/squirtles/musicroad/search/SearchMusicScreen.kt rename to feature/search/src/main/java/com/squirtles/feature/search/SearchMusicScreen.kt index d76a66eb..361234d0 100644 --- a/app/src/main/java/com/squirtles/musicroad/search/SearchMusicScreen.kt +++ b/feature/search/src/main/java/com/squirtles/feature/search/SearchMusicScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.search +package com.squirtles.feature.search import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -55,16 +55,18 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import com.squirtles.domain.model.Song -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.AlbumImage -import com.squirtles.musicroad.common.Constants.COLOR_STOPS -import com.squirtles.musicroad.common.Constants.REQUEST_IMAGE_SIZE_DEFAULT -import com.squirtles.musicroad.common.HorizontalSpacer -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.create.SearchUiState -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.AlbumImage +import com.squirtles.core.common.ui.Constants.COLOR_STOPS +import com.squirtles.core.common.ui.Constants.REQUEST_IMAGE_SIZE_DEFAULT +import com.squirtles.core.common.ui.HorizontalSpacer +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.White +import com.squirtles.core.model.Song +import com.squirtles.feature.search.SearchUiConstants.DefaultPadding +import com.squirtles.feature.search.SearchUiConstants.ImageSize +import com.squirtles.feature.search.SearchUiConstants.ItemSpacing +import com.squirtles.feature.search.SearchUiConstants.SearchBarHeight @Composable fun SearchMusicScreen( @@ -334,8 +336,3 @@ fun SongItemPreview() { } } - -private val SearchBarHeight = 56.dp -private val DefaultPadding = 16.dp -private val ItemSpacing = 24.dp -private val ImageSize = 56.dp diff --git a/feature/search/src/main/java/com/squirtles/feature/search/SearchUiConstants.kt b/feature/search/src/main/java/com/squirtles/feature/search/SearchUiConstants.kt new file mode 100644 index 00000000..2933270e --- /dev/null +++ b/feature/search/src/main/java/com/squirtles/feature/search/SearchUiConstants.kt @@ -0,0 +1,12 @@ +package com.squirtles.feature.search + +import androidx.compose.ui.unit.dp + +object SearchUiConstants { + val SearchBarHeight = 56.dp + val DefaultPadding = 16.dp + val ItemSpacing = 24.dp + val ImageSize = 56.dp +} + + diff --git a/feature/search/src/main/java/com/squirtles/feature/search/SearchUiState.kt b/feature/search/src/main/java/com/squirtles/feature/search/SearchUiState.kt new file mode 100644 index 00000000..956bbb94 --- /dev/null +++ b/feature/search/src/main/java/com/squirtles/feature/search/SearchUiState.kt @@ -0,0 +1,6 @@ +package com.squirtles.feature.search + +sealed class SearchUiState { + data object HotResult : SearchUiState() + data object SearchResult : SearchUiState() +} diff --git a/app/src/main/java/com/squirtles/musicroad/search/SearchViewModel.kt b/feature/search/src/main/java/com/squirtles/feature/search/SearchViewModel.kt similarity index 91% rename from app/src/main/java/com/squirtles/musicroad/search/SearchViewModel.kt rename to feature/search/src/main/java/com/squirtles/feature/search/SearchViewModel.kt index 5e3a7905..77e11936 100644 --- a/app/src/main/java/com/squirtles/musicroad/search/SearchViewModel.kt +++ b/feature/search/src/main/java/com/squirtles/feature/search/SearchViewModel.kt @@ -1,12 +1,11 @@ -package com.squirtles.musicroad.search +package com.squirtles.feature.search import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn -import com.squirtles.domain.model.Song -import com.squirtles.domain.usecase.music.FetchSongsUseCase -import com.squirtles.musicroad.create.SearchUiState +import com.squirtles.domain.applemusic.usecase.FetchSongsUseCase +import com.squirtles.core.model.Song import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/feature/search/src/main/java/com/squirtles/feature/search/navigation/SearchNavigation.kt b/feature/search/src/main/java/com/squirtles/feature/search/navigation/SearchNavigation.kt new file mode 100644 index 00000000..f5deca88 --- /dev/null +++ b/feature/search/src/main/java/com/squirtles/feature/search/navigation/SearchNavigation.kt @@ -0,0 +1,25 @@ +package com.squirtles.feature.search.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.squirtles.core.model.Song +import com.squirtles.core.navigation.MainRoute +import com.squirtles.feature.search.SearchMusicScreen + +fun NavController.navigateSearch(navOptions: NavOptions? = null) { + navigate(MainRoute.Search, navOptions) +} + +fun NavGraphBuilder.searchNavGraph( + onBackClick: () -> Unit, + onItemClick: (Song) -> Unit, +) { + composable { + SearchMusicScreen( + onBackClick = onBackClick, + onItemClick = onItemClick, // Create 이동 + ) + } +} diff --git a/feature/search/src/main/res/values/strings.xml b/feature/search/src/main/res/values/strings.xml new file mode 100644 index 00000000..08bbf77d --- /dev/null +++ b/feature/search/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + MusicRoad + + 상단 바 뒤로 가기 버튼 + + + 검색 + 검색 결과 + 노래 앨범 이미지 + 노래 검색 버튼 + 검색 결과가 없습니다. + 검색 결과를 불러올 수 없습니다. + diff --git a/feature/search/src/test/java/com/squirtles/feature/search/ExampleUnitTest.kt b/feature/search/src/test/java/com/squirtles/feature/search/ExampleUnitTest.kt new file mode 100644 index 00000000..7993c561 --- /dev/null +++ b/feature/search/src/test/java/com/squirtles/feature/search/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.example.search + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/feature/userinfo/.gitignore b/feature/userinfo/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/userinfo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/userinfo/build.gradle.kts b/feature/userinfo/build.gradle.kts new file mode 100644 index 00000000..3cf86c4c --- /dev/null +++ b/feature/userinfo/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.musicroad.feature) +} + +android { + namespace = "com.squirtles.feature.userinfo" +} + +dependencies { + implementation(projects.core.account) + implementation(projects.domain.user) + + implementation(libs.coil) + implementation(libs.coil.compose) + + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.test) +} diff --git a/feature/userinfo/proguard-rules.pro b/feature/userinfo/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/userinfo/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/userinfo/src/androidTest/java/com/squirtles/feature/userinfo/ExampleInstrumentedTest.kt b/feature/userinfo/src/androidTest/java/com/squirtles/feature/userinfo/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..8fdfeaa9 --- /dev/null +++ b/feature/userinfo/src/androidTest/java/com/squirtles/feature/userinfo/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.squirtles.userinfo + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.squirtles.userinfo.test", appContext.packageName) + } +} diff --git a/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/UserInfoConstants.kt b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/UserInfoConstants.kt new file mode 100644 index 00000000..c8535066 --- /dev/null +++ b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/UserInfoConstants.kt @@ -0,0 +1,13 @@ +package com.squirtles.feature.userinfo + +import androidx.compose.ui.unit.dp +import com.squirtles.core.model.User + +internal object UserInfoConstants { + const val USERNAME_PATTERN = "^[ㄱ-ㅎ|ㅏ-ㅣ가-힣a-zA-Z0-9]+$" + val DEFAULT_USER = User("", "", "", null, listOf()) + + // UI + val MENU_PADDING_HORIZONTAL = 24.dp + val MENU_PADDING_VERTICAL = 8.dp +} diff --git a/app/src/main/java/com/squirtles/musicroad/userinfo/UserInfoViewModel.kt b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/UserInfoViewModel.kt similarity index 82% rename from app/src/main/java/com/squirtles/musicroad/userinfo/UserInfoViewModel.kt rename to feature/userinfo/src/main/java/com/squirtles/feature/userinfo/UserInfoViewModel.kt index 46162cee..1670387b 100644 --- a/app/src/main/java/com/squirtles/musicroad/userinfo/UserInfoViewModel.kt +++ b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/UserInfoViewModel.kt @@ -1,11 +1,11 @@ -package com.squirtles.musicroad.userinfo +package com.squirtles.feature.userinfo import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.squirtles.domain.model.User -import com.squirtles.domain.usecase.user.FetchUserByIdUseCase -import com.squirtles.domain.usecase.user.GetCurrentUidUseCase -import com.squirtles.domain.usecase.user.UpdateUserNameUseCase +import com.squirtles.domain.user.usecase.FetchUserByIdUseCase +import com.squirtles.domain.user.usecase.GetCurrentUidUseCase +import com.squirtles.domain.user.usecase.UpdateUserNameUseCase +import com.squirtles.feature.userinfo.UserInfoConstants.DEFAULT_USER import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -49,5 +49,3 @@ class UserInfoViewModel @Inject constructor( } } -val DEFAULT_USER = User("", "", "", null, listOf()) - diff --git a/app/src/main/java/com/squirtles/musicroad/userinfo/components/MenuItem.kt b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/components/MenuItem.kt similarity index 74% rename from app/src/main/java/com/squirtles/musicroad/userinfo/components/MenuItem.kt rename to feature/userinfo/src/main/java/com/squirtles/feature/userinfo/components/MenuItem.kt index ffa4050b..e8278f72 100644 --- a/app/src/main/java/com/squirtles/musicroad/userinfo/components/MenuItem.kt +++ b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/components/MenuItem.kt @@ -1,8 +1,8 @@ -package com.squirtles.musicroad.userinfo.components +package com.squirtles.feature.userinfo.components import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.theme.White data class MenuItem( val imageVector: ImageVector, diff --git a/app/src/main/java/com/squirtles/musicroad/userinfo/components/UserInfoMenus.kt b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/components/UserInfoMenus.kt similarity index 88% rename from app/src/main/java/com/squirtles/musicroad/userinfo/components/UserInfoMenus.kt rename to feature/userinfo/src/main/java/com/squirtles/feature/userinfo/components/UserInfoMenus.kt index c8bb9e32..e2ee38ca 100644 --- a/app/src/main/java/com/squirtles/musicroad/userinfo/components/UserInfoMenus.kt +++ b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/components/UserInfoMenus.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.userinfo.components +package com.squirtles.feature.userinfo.components import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -24,11 +24,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.Constants.DEFAULT_PADDING -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.Constants.DEFAULT_PADDING +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.userinfo.R +import com.squirtles.feature.userinfo.UserInfoConstants.MENU_PADDING_HORIZONTAL +import com.squirtles.feature.userinfo.UserInfoConstants.MENU_PADDING_VERTICAL @Composable internal fun UserInfoMenus( @@ -99,6 +101,3 @@ internal fun UserInfoMenus( VerticalSpacer(20) } } - -private val MENU_PADDING_HORIZONTAL = 24.dp -private val MENU_PADDING_VERTICAL = 8.dp diff --git a/app/src/main/java/com/squirtles/musicroad/userinfo/navigation/UserInfoNavigation.kt b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/navigation/UserInfoNavigation.kt similarity index 68% rename from app/src/main/java/com/squirtles/musicroad/userinfo/navigation/UserInfoNavigation.kt rename to feature/userinfo/src/main/java/com/squirtles/feature/userinfo/navigation/UserInfoNavigation.kt index 5c357224..53014e14 100644 --- a/app/src/main/java/com/squirtles/musicroad/userinfo/navigation/UserInfoNavigation.kt +++ b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/navigation/UserInfoNavigation.kt @@ -1,16 +1,15 @@ -package com.squirtles.musicroad.userinfo.navigation +package com.squirtles.feature.userinfo.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.squirtles.musicroad.mypick.MyPickScreen -import com.squirtles.musicroad.navigation.MainRoute -import com.squirtles.musicroad.navigation.UserInfoRoute -import com.squirtles.musicroad.userinfo.screen.EditNotificationSettingScreen -import com.squirtles.musicroad.userinfo.screen.EditProfileScreen -import com.squirtles.musicroad.userinfo.screen.UserInfoScreen +import com.squirtles.core.navigation.MainRoute +import com.squirtles.core.navigation.UserInfoRoute +import com.squirtles.feature.userinfo.screen.EditNotificationSettingScreen +import com.squirtles.feature.userinfo.screen.EditProfileScreen +import com.squirtles.feature.userinfo.screen.UserInfoScreen fun NavController.navigateUserInfo(uid: String, navOptions: NavOptions? = null) { navigate(MainRoute.UserInfo(uid), navOptions) @@ -24,13 +23,8 @@ fun NavController.navigateEditNotificationSetting(navOptions: NavOptions? = null navigate(UserInfoRoute.EditNotification, navOptions) } -fun NavController.navigateMyPicks(uid: String, navOptions: NavOptions) { - navigate(UserInfoRoute.MyPicks(uid), navOptions) -} - fun NavGraphBuilder.userInfoNavGraph( onBackClick: () -> Unit, - onItemClick: (String) -> Unit, onBackToMapClick: () -> Unit, onFavoritePicksClick: (String) -> Unit, onMyPicksClick: (String) -> Unit, @@ -65,14 +59,4 @@ fun NavGraphBuilder.userInfoNavGraph( onBackClick = onBackClick ) } - - composable { backStackEntry -> - val uid = backStackEntry.toRoute().uid - - MyPickScreen( - uid = uid, - onBackClick = onBackClick, - onItemClick = onItemClick - ) - } } diff --git a/app/src/main/java/com/squirtles/musicroad/userinfo/screen/EditNotificationSettingScreen.kt b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/screen/EditNotificationSettingScreen.kt similarity index 82% rename from app/src/main/java/com/squirtles/musicroad/userinfo/screen/EditNotificationSettingScreen.kt rename to feature/userinfo/src/main/java/com/squirtles/feature/userinfo/screen/EditNotificationSettingScreen.kt index fb3a55c3..45b26356 100644 --- a/app/src/main/java/com/squirtles/musicroad/userinfo/screen/EditNotificationSettingScreen.kt +++ b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/screen/EditNotificationSettingScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.userinfo.screen +package com.squirtles.feature.userinfo.screen import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.res.stringResource import androidx.wear.compose.material.Text -import com.squirtles.musicroad.R -import com.squirtles.musicroad.common.Constants.COLOR_STOPS -import com.squirtles.musicroad.common.Constants.DEFAULT_PADDING -import com.squirtles.musicroad.common.DefaultTopAppBar -import com.squirtles.musicroad.ui.theme.White +import com.squirtles.core.common.ui.Constants.COLOR_STOPS +import com.squirtles.core.common.ui.Constants.DEFAULT_PADDING +import com.squirtles.core.common.ui.DefaultTopAppBar +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.userinfo.R @Composable internal fun EditNotificationSettingScreen( diff --git a/app/src/main/java/com/squirtles/musicroad/userinfo/screen/EditProfileScreen.kt b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/screen/EditProfileScreen.kt similarity index 92% rename from app/src/main/java/com/squirtles/musicroad/userinfo/screen/EditProfileScreen.kt rename to feature/userinfo/src/main/java/com/squirtles/feature/userinfo/screen/EditProfileScreen.kt index c2df7708..d75d57e0 100644 --- a/app/src/main/java/com/squirtles/musicroad/userinfo/screen/EditProfileScreen.kt +++ b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/screen/EditProfileScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.userinfo.screen +package com.squirtles.feature.userinfo.screen import android.content.Context import android.widget.Toast @@ -51,20 +51,21 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.flowWithLifecycle -import com.squirtles.musicroad.R -import com.squirtles.musicroad.account.AccountViewModel -import com.squirtles.musicroad.account.GoogleId -import com.squirtles.musicroad.common.Constants.COLOR_STOPS -import com.squirtles.musicroad.common.DialogTextButton -import com.squirtles.musicroad.common.HorizontalSpacer -import com.squirtles.musicroad.common.MessageAlertDialog -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.DarkGray -import com.squirtles.musicroad.ui.theme.Gray -import com.squirtles.musicroad.ui.theme.MusicRoadTheme -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White -import com.squirtles.musicroad.userinfo.UserInfoViewModel +import com.squirtles.core.account.AccountViewModel +import com.squirtles.core.account.GoogleId +import com.squirtles.core.common.ui.Constants.COLOR_STOPS +import com.squirtles.core.common.ui.DialogTextButton +import com.squirtles.core.common.ui.HorizontalSpacer +import com.squirtles.core.common.ui.MessageAlertDialog +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.DarkGray +import com.squirtles.core.common.ui.theme.Gray +import com.squirtles.core.common.ui.theme.MusicRoadTheme +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.userinfo.R +import com.squirtles.feature.userinfo.UserInfoConstants.USERNAME_PATTERN +import com.squirtles.feature.userinfo.UserInfoViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.regex.Pattern @@ -188,7 +189,6 @@ internal fun EditProfileScreen( }, title = stringResource(R.string.delete_account_dialog_title), body = stringResource(R.string.delete_account_dialog_description), - showBody = true ) { DialogTextButton( onClick = { @@ -308,8 +308,6 @@ private fun EditProfileContents( } } -private const val USERNAME_PATTERN = "^[ㄱ-ㅎ|ㅏ-ㅣ가-힣a-zA-Z0-9]+$" - @Preview @Composable private fun EditProfileAppBarPreview() { diff --git a/app/src/main/java/com/squirtles/musicroad/userinfo/screen/UserInfoScreen.kt b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/screen/UserInfoScreen.kt similarity index 92% rename from app/src/main/java/com/squirtles/musicroad/userinfo/screen/UserInfoScreen.kt rename to feature/userinfo/src/main/java/com/squirtles/feature/userinfo/screen/UserInfoScreen.kt index 4b8540dc..7ad62f67 100644 --- a/app/src/main/java/com/squirtles/musicroad/userinfo/screen/UserInfoScreen.kt +++ b/feature/userinfo/src/main/java/com/squirtles/feature/userinfo/screen/UserInfoScreen.kt @@ -1,4 +1,4 @@ -package com.squirtles.musicroad.userinfo.screen +package com.squirtles.feature.userinfo.screen import androidx.activity.compose.BackHandler import androidx.compose.foundation.background @@ -51,21 +51,21 @@ import androidx.lifecycle.flowWithLifecycle import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade -import com.squirtles.musicroad.R -import com.squirtles.musicroad.account.AccountViewModel -import com.squirtles.musicroad.account.GoogleId -import com.squirtles.musicroad.common.Constants.COLOR_STOPS -import com.squirtles.musicroad.common.DefaultTopAppBar -import com.squirtles.musicroad.common.DialogTextButton -import com.squirtles.musicroad.common.HorizontalSpacer -import com.squirtles.musicroad.common.MessageAlertDialog -import com.squirtles.musicroad.common.VerticalSpacer -import com.squirtles.musicroad.ui.theme.Black -import com.squirtles.musicroad.ui.theme.Primary -import com.squirtles.musicroad.ui.theme.White -import com.squirtles.musicroad.userinfo.UserInfoViewModel -import com.squirtles.musicroad.userinfo.components.MenuItem -import com.squirtles.musicroad.userinfo.components.UserInfoMenus +import com.squirtles.core.account.AccountViewModel +import com.squirtles.core.account.GoogleId +import com.squirtles.core.common.ui.Constants.COLOR_STOPS +import com.squirtles.core.common.ui.DefaultTopAppBar +import com.squirtles.core.common.ui.DialogTextButton +import com.squirtles.core.common.ui.HorizontalSpacer +import com.squirtles.core.common.ui.MessageAlertDialog +import com.squirtles.core.common.ui.VerticalSpacer +import com.squirtles.core.common.ui.theme.Black +import com.squirtles.core.common.ui.theme.Primary +import com.squirtles.core.common.ui.theme.White +import com.squirtles.feature.userinfo.R +import com.squirtles.feature.userinfo.UserInfoViewModel +import com.squirtles.feature.userinfo.components.MenuItem +import com.squirtles.feature.userinfo.components.UserInfoMenus import kotlinx.coroutines.launch @Composable diff --git a/app/src/main/res/drawable/img_user_default_profile.jpg b/feature/userinfo/src/main/res/drawable/img_user_default_profile.jpg similarity index 100% rename from app/src/main/res/drawable/img_user_default_profile.jpg rename to feature/userinfo/src/main/res/drawable/img_user_default_profile.jpg diff --git a/feature/userinfo/src/main/res/values/strings.xml b/feature/userinfo/src/main/res/values/strings.xml new file mode 100644 index 00000000..3c6d6d9a --- /dev/null +++ b/feature/userinfo/src/main/res/values/strings.xml @@ -0,0 +1,48 @@ + + MusicRoad + + + 상단 바 뒤로 가기 버튼 + + + 메뉴로 이동하기 아이콘 + 프로필 이미지 + Pick + 픽 보관함 + 등록한 픽 + 픽 보관함 메뉴 아이콘 + 등록한 픽 메뉴 아이콘 + Settings + 프로필 + 알림 + 로그아웃 + 회원 탈퇴 + 프로필 설정 메뉴 아이콘 + 알림 메뉴 아이콘 + 로그아웃 메뉴 아이콘 + 지도 아이콘 + 지도로 돌아가기 + + + 알림 설정 + 준비 중인 기능입니다! + 프로필 설정 + 닉네임 + 닉네임에는 한글, 영문, 숫자만 사용할 수 있습니다. + 닉네임을 2글자 이상 입력해주세요. + 닉네임은 10글자 이하로 가능합니다. + 변경 사항 적용 + 변경 사항이 적용되었습니다. + 일시적인 오류가 발생했습니다. + + + 로그아웃 하시겠습니까? + 취소 + 로그아웃 + + 정말 탈퇴하시겠습니까? + 탈퇴 시 회원 정보와 픽 데이터가 삭제되며, 복구할 수 없습니다. + 취소 + 탈퇴 + + diff --git a/feature/userinfo/src/test/java/com/squirtles/feature/userinfo/ExampleUnitTest.kt b/feature/userinfo/src/test/java/com/squirtles/feature/userinfo/ExampleUnitTest.kt new file mode 100644 index 00000000..d2f41332 --- /dev/null +++ b/feature/userinfo/src/test/java/com/squirtles/feature/userinfo/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.squirtles.userinfo + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/gradle.properties b/gradle.properties index 20e2a015..2d0095c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. For more details, visit # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects @@ -20,4 +20,4 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 30400a18..49867684 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,99 +1,176 @@ [versions] -activityCompose = "1.9.3" + +# SDK versions +compileSdk = "34" +minSdk = "26" +targetSdk = "34" +jvmTarget = "17" +jdkVersion = "17" + +# App Version +versionCode = "10100" +versionName = "1.1.0" + +# Build agp = "8.5.1" -animation = "1.7.5" -appcompat = "1.7.0" -coil = "3.0.0" +androidTools = "31.2.0" + +# AndroidX +androidx-core-ktx = "1.13.1" +androidx-lifecycle = "2.8.3" +androidx-lifecycle-runtime-ktx = "2.8.7" +androidx-datastore-preferences = "1.1.1" +androidx-appcompat = "1.7.0" +androidx-navigation-common-ktx = "2.8.5" + +# Splash +androidx-core-splashscreen = "1.0.1" + +# Media3 +media3Ui = "1.4.1" + +# Compose +compose-compiler = "1.5.14" +androidx-compose-animation = "1.7.5" +activityCompose = "1.9.3" composeBom = "2024.10.01" composeMaterial = "1.4.0" -constraintlayout = "2.2.0" -coreKtx = "1.13.1" -credentials = "1.3.0" -datastorePreferences = "1.1.1" -espressoCore = "3.6.1" +navigationCompose = "2.8.3" +compose-runtime-android = "1.7.6" + +# Kotlin +kotlin = "1.9.24" +kotlinxImmutable = "0.3.7" +jetbrainsKotlinJvm = "1.9.22" +ksp = "1.9.24-1.0.20" + +# Coroutines +kotlinxCoroutinesPlayServices = "1.9.0" +kotlinxCoroutinesCore = "1.9.0" + +# Coil +coil = "3.0.0" + +# Firebase firebaseBom = "33.5.1" firebaseDynamicModuleSupportVersion = "16.0.0-beta03" firebaseFirestoreKtx = "25.1.1" firebaseFunctionsKtx = "21.1.0" firebaseCrashlytics = "3.0.2" geofireAndroidCommon = "3.2.0" -googleid = "1.1.1" -googleServices = "4.4.2" +firebaseAuthKtx = "22.3.1" + +# Hilt hilt = "2.51.1" hiltNavigationCompose = "1.2.0" inject = "1" + +# Test +espressoCore = "3.5.0" junit = "4.13.2" -junitVersion = "1.2.1" -kotlin = "1.9.0" -kotlinxCoroutinesPlayServices = "1.9.0" -kotlinxCoroutinesCore = "1.9.0" -kotlinxCoroutinesGuava = "1.9.0" +junitVersion = "1.1.5" + +# Google +googleServices = "4.4.2" + +# Account +androidx-credentials = "1.3.0" +googleid = "1.1.1" + +# Paging +paging = "3.3.4" + +# Serialization kotlinxSerializationJson = "1.6.0" -ksp = "1.9.0-1.0.12" -lifecycleRuntimeKtx = "2.8.7" + +# Map mapSdk = "3.19.1" -material = "1.12.0" -media3Ui = "1.4.1" -navigationCompose = "2.8.3" -okhttp = "4.12.0" -pagingRuntime = "3.3.4" playServicesLocation = "21.3.0" + +# Network +okhttp = "4.12.0" retrofit = "2.11.0" -splashscreen = "1.0.1" -uiViewbinding = "1.7.5" -pagingComposeAndroid = "3.3.4" -kotlinxImmutable = "0.3.7" -firebaseAuthKtx = "22.3.1" + +material = "1.12.0" +composeMaterial3 = "1.3.1" +composeMaterialIconsExtended = "1.7.8" +animationCoreAndroid = "1.7.8" +foundationLayoutAndroid = "1.7.8" +foundationAndroid = "1.7.8" [libraries] # AndroidX -androidx-animation = { module = "androidx.compose.animation:animation", version.ref = "animation" } -androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -androidx-credentials = { module = "androidx.credentials:credentials", version.ref = "credentials" } -androidx-credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentials" } -androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } -androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } -androidx-hilt-navigation-fragment = { module = "androidx.hilt:hilt-navigation-fragment", version.ref = "hiltNavigationCompose" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Ui" } -androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "media3Ui" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore-preferences" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-ktx" } +androidx-navigation-common-ktx = { group = "androidx.navigation", name = "navigation-common-ktx", version.ref = "androidx-navigation-common-ktx" } + +# Splash +androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" } + +# Media3 androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" } -androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "pagingRuntime" } androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3Ui" } -androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Ui" } # for media3-common -androidx-ui = { group = "androidx.compose.ui", name = "ui" } -androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } -androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } -androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } -androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } +androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Ui" } + +# Exoplayer +androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Ui" } +androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "media3Ui" } + +# Di +inject = { group = "javax.inject", name = "javax.inject", version.ref = "inject" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } +hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } +hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } + +# Compose +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } +compose-ui = { group = "androidx.compose.ui", name = "ui" } +compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +compose-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "compose-runtime-android" } +kotlinx-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" } +androidx-animation-core-android = { group = "androidx.compose.animation", name = "animation-core-android", version.ref = "animationCoreAndroid" } +androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundationLayoutAndroid" } + +# Paging +androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging" } +androidx-paging-compose = { group = "androidx.paging", name = "paging-compose-android", version.ref = "paging" } + +# Auth +androidx-credentials = { module = "androidx.credentials:credentials", version.ref = "androidx-credentials" } +androidx-credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "androidx-credentials" } googleid = { module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "googleid" } + +# Coroutines kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } -kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" } -kotlinx-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesCore" } # Firebase -firebase-analytics = { module = "com.google.firebase:firebase-analytics" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } firebase-firestore-ktx = { group = "com.google.firebase", name = "firebase-firestore-ktx", version.ref = "firebaseFirestoreKtx" } firebase-functions-ktx = { group = "com.google.firebase", name = "firebase-functions-ktx", version.ref = "firebaseFunctionsKtx" } firebase-auth-ktx = { group = "com.google.firebase", name = "firebase-auth-ktx", version.ref = "firebaseAuthKtx" } -kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesPlayServices" } +google-firebase-dynamic-module-support = { module = "com.google.firebase:firebase-dynamic-module-support", version.ref = "firebaseDynamicModuleSupportVersion" } + +# goefire +geofire-android-common = { module = "com.firebase:geofire-android-common", version.ref = "geofireAndroidCommon" } + +# Analytics firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics" } # Test -geofire-android-common = { module = "com.firebase:geofire-android-common", version.ref = "geofireAndroidCommon" } -google-firebase-dynamic-module-support = { module = "com.google.firebase:firebase-dynamic-module-support", version.ref = "firebaseDynamicModuleSupportVersion" } junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } # Map map-sdk = { module = "com.naver.maps:map-sdk", version.ref = "mapSdk" } @@ -101,35 +178,128 @@ play-services-location = { module = "com.google.android.gms:play-services-locati # Material Design material = { group = "com.google.android.material", name = "material", version.ref = "material" } -androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "composeMaterial3" } +compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } +compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "composeMaterialIconsExtended" } -# Hilt -inject = { group = "javax.inject", name = "javax.inject", version.ref = "inject" } -hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } -hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } -hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } - -# OkHttp +# Network +okhttp = { group = "com.squareup.okhttp3", name = "okhttp" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } - -# Retrofit retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-kotlin-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } # Kotlinx Serialization kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -# ConstraintLayout -androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } - -# UI ViewBinding -androidx-ui-viewbinding = { group = "androidx.compose.ui", name = "ui-viewbinding", version.ref = "uiViewbinding" } - # Coil coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } -androidx-paging-compose-android = { group = "androidx.paging", name = "paging-compose-android", version.ref = "pagingComposeAndroid" } + +# Build-logic +android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } +android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" } +kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } +androidx-foundation-android = { group = "androidx.compose.foundation", name = "foundation-android", version.ref = "foundationAndroid" } + +[bundles] +androidx-core = [ + "androidx-appcompat", + "androidx-core-ktx", + "androidx-lifecycle-runtime-ktx", +] + +analytics = [ + "firebase-analytics", + "firebase-crashlytics", +] + +auth = [ + "androidx-credentials", + "androidx-credentials-play-services-auth", + "googleid", +] + +compose = [ + "androidx-activity-compose", + "androidx-lifecycle-viewmodel-compose", + "compose-runtime-android", + "compose-ui", + "compose-ui-tooling", + "compose-ui-tooling-preview", +] + +compose-debug = [ + "compose-ui-test-junit4", + "compose-ui-test-manifest", +] + +coroutines = [ + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", +] + +datastore = [ + "androidx-datastore-preferences", +] + +exoplayer = [ + "androidx-media3-exoplayer", + "androidx-media3-exoplayer-dash", +] + +firebase = [ + "firebase-firestore-ktx", + "firebase-functions-ktx", + "google-firebase-dynamic-module-support", +] + +map = [ + "map-sdk", + "play-services-location", +] + +material = [ + "compose-material", + "compose-material-icons-extended", + "compose-material3", + "material", +] + +media3 = [ + "androidx-media3-common", + "androidx-media3-session", + "androidx-media3-ui", +] + +navigation = [ + "hilt-navigation-compose", + "navigation-compose", +] + +network = [ + "okhttp", + "okhttp-logging", + "retrofit-core", + "retrofit-kotlin-serialization", +] + +coil = [ + "coil", + "coil-compose", + "coil-network-okhttp", +] + +test = [ + "androidx-junit", + "androidx-espresso-core", +] + +paging = [ + "androidx-paging-compose", + "androidx-paging-runtime", +] # Plugins [plugins] @@ -140,4 +310,14 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } -firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } \ No newline at end of file +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } +jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" } + +# Custom Plugins +musicroad-android-application = { id = "musicroad.android.application", version = "unspecified" } +musicroad-java-library = { id = "musicroad.java.library", version = "unspecified" } +musicroad-android-library = { id = "musicroad.android.library", version = "unspecified" } +musicroad-compose-library = { id = "musicroad.compose.library", version = "unspecified" } +musicroad-hilt = { id = "musicroad.hilt", version = "unspecified" } +musicroad-data = { id = "musicroad.data", version = "unspecified" } +musicroad-feature = { id = "musicroad.feature", version = "unspecified" } diff --git a/mediaservice/consumer-rules.pro b/mediaservice/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/mediaservice/src/main/AndroidManifest.xml b/mediaservice/src/main/AndroidManifest.xml deleted file mode 100644 index 8bdb7e14..00000000 --- a/mediaservice/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/mediaservice/src/main/res/drawable/ic_musicroad_foreground.xml b/mediaservice/src/main/res/drawable/ic_musicroad_foreground.xml deleted file mode 100644 index ba25c130..00000000 --- a/mediaservice/src/main/res/drawable/ic_musicroad_foreground.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/settings.gradle.kts b/settings.gradle.kts index f0a67411..887c4984 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,7 @@ pluginManagement { + + includeBuild("build-logic") + repositories { google { content { @@ -17,12 +20,49 @@ dependencyResolutionManagement { google() mavenCentral() maven("https://repository.map.naver.com/archive/maven") + gradlePluginPortal() } } +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") +gradle.startParameter.excludedTaskNames.addAll(listOf(":build-logic:convention:testClasses")) + rootProject.name = "MusicRoad" include(":app") include(":domain") include(":data") -include(":mediaservice") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") +include(":core:model") +include(":core:navigation") +include(":core:util") +include(":core:common") +include(":core:picklist") +include(":core:mediaservice") +include(":domain:picklist") +include(":domain:applemusic") +include(":domain:favorite") +include(":domain:location") +include(":domain:order") +include(":domain:pick") +include(":domain:user") +include(":domain:player") +include(":data:applemusic") +include(":data:favorite") +include(":data:firebase") +include(":data:location") +include(":data:order") +include(":data:pick") +include(":data:user") +include(":core:musicplayer") +include(":core:account") +include(":feature:create") +include(":feature:favorite") +include(":core:buildconfig") +include(":feature:search") +include(":feature:userinfo") +include(":feature:mypick") +include(":audio_visualizer") +include(":feature:detail") +include(":feature:map") +include(":feature:main") +include(":domain:firebase")