diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index dfbd650..6501d8c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -125,9 +125,9 @@ dependencies {
implementation(libs.bundles.ktor)
// //---------------Media3---------------//
-// implementation(libs.bundles.media3)
-// implementation(libs.androidx.media3.datasource.okhttp)
-
+ implementation(libs.bundles.media3)
+ implementation(libs.androidx.media3.datasource.okhttp)
+ implementation(project(":app:mediaplayer"))
//---------------Dependency Injection---------------//
implementation(libs.bundles.hilt)
diff --git a/app/mediaplayer/build.gradle.kts b/app/mediaplayer/build.gradle.kts
index 5dc1644..6ef6eaa 100644
--- a/app/mediaplayer/build.gradle.kts
+++ b/app/mediaplayer/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.kotlin.ksp)
+ alias(libs.plugins.hilt)
}
android {
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaService.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaplayerService.kt
similarity index 96%
rename from app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaService.kt
rename to app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaplayerService.kt
index c7f7a5a..22d3055 100644
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaService.kt
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaplayerService.kt
@@ -10,7 +10,7 @@ import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
-class MediaService : MediaSessionService() {
+class MediaplayerService : MediaSessionService() {
@Inject
lateinit var mediaSession: MediaSession
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f7e097d..f0417a6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,6 +14,9 @@
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt b/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt
index cddd943..aef141d 100644
--- a/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt
@@ -1,5 +1,6 @@
package com.bobbyesp.metadator
+import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -9,6 +10,7 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
+import com.bobbyesp.mediaplayer.service.MediaplayerService
import com.bobbyesp.metadator.presentation.Navigator
import com.bobbyesp.metadator.presentation.common.AppLocalSettingsProvider
import com.bobbyesp.metadator.presentation.common.LocalDarkTheme
@@ -18,6 +20,8 @@ import setupFirebase
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
+ private var isMusicPlayerServiceStarted = false
+
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
@@ -43,6 +47,19 @@ class MainActivity : ComponentActivity() {
}
}
+ override fun onDestroy() {
+ super.onDestroy()
+ stopService(Intent(this, MediaplayerService::class.java))
+ isMusicPlayerServiceStarted = false
+ }
+
+ fun startMediaPlayerService() {
+ if (!isMusicPlayerServiceStarted) {
+ isMusicPlayerServiceStarted = true
+ startService(Intent(this, MediaplayerService::class.java))
+ }
+ }
+
companion object {
private lateinit var activity: MainActivity
fun getActivity(): MainActivity {
diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/Navigation.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/Navigation.kt
index 0fdee91..a0f7a6a 100644
--- a/app/src/main/java/com/bobbyesp/metadator/presentation/Navigation.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/presentation/Navigation.kt
@@ -22,6 +22,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -47,11 +48,13 @@ import com.bobbyesp.metadator.presentation.common.TagEditorParcelableSongParamTy
import com.bobbyesp.metadator.presentation.common.routesToNavigate
import com.bobbyesp.metadator.presentation.pages.MediaStorePageViewModel
import com.bobbyesp.metadator.presentation.pages.home.HomePage
+import com.bobbyesp.metadator.presentation.pages.mediaplayer.MediaplayerPage
import com.bobbyesp.metadator.presentation.pages.utilities.tageditor.ID3MetadataEditorPage
import com.bobbyesp.metadator.presentation.pages.utilities.tageditor.ID3MetadataEditorPageViewModel
import com.bobbyesp.ui.motion.animatedComposable
import com.bobbyesp.ui.motion.slideInVerticallyComposable
import com.bobbyesp.utilities.navigation.getParcelable
+import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
@@ -73,6 +76,8 @@ fun Navigator() {
val snackbarHostState = LocalSnackbarHostState.current
+ val scope = rememberCoroutineScope()
+
val showSnackbarMessage: suspend (String) -> Unit = { message ->
snackbarHostState.showSnackbar(message)
}
@@ -108,35 +113,47 @@ fun Navigator() {
fontFamily = FontFamily.Monospace
)
routesToNavigate.forEachIndexed { _, route ->
- NavigationDrawerItem(label = {
- Text(text = route.title?.let { stringResource(id = it) } ?: "")
- }, selected = currentRootRoute.value == route.route, onClick = {
-
- }, icon = {
- Icon(imageVector = route.icon!!,
- contentDescription = route.title?.let { stringResource(id = it) })
- }, badge = {
+ NavigationDrawerItem(
+ label = {
+ Text(text = route.title?.let { stringResource(id = it) } ?: "")
+ }, selected = currentRootRoute.value == route.route, onClick = {
+ if (currentRootRoute.value == route.route) {
+ scope.launch {
+ drawerState.close()
+ }
+ return@NavigationDrawerItem
+ } else {
+ navController.navigate(route.route) {
+ popUpTo(navController.graph.startDestinationId)
+ launchSingleTop = true
+ }
+ scope.launch {
+ drawerState.close()
+ }
+ }
+ }, icon = {
+ Icon(imageVector = route.icon!!,
+ contentDescription = route.title?.let { stringResource(id = it) })
+ }, badge = {
- }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
+ }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
},
) {
- Scaffold(
- snackbarHost = {
- SnackbarHost(
- hostState = snackbarHostState
- ) { dataReceived ->
- Snackbar(
- modifier = Modifier,
- snackbarData = dataReceived,
- containerColor = MaterialTheme.colorScheme.inverseSurface,
- contentColor = MaterialTheme.colorScheme.inverseOnSurface,
- )
- }
+ Scaffold(snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState
+ ) { dataReceived ->
+ Snackbar(
+ modifier = Modifier,
+ snackbarData = dataReceived,
+ containerColor = MaterialTheme.colorScheme.inverseSurface,
+ contentColor = MaterialTheme.colorScheme.inverseOnSurface,
+ )
}
- ) {
+ }) {
NavHost(
modifier = Modifier
.fillMaxWidth()
@@ -154,6 +171,15 @@ fun Navigator() {
}
}
+ navigation(
+ startDestination = Route.MediaplayerNavigator.Mediaplayer.route,
+ route = Route.MediaplayerNavigator.route
+ ) {
+ animatedComposable(Route.MediaplayerNavigator.Mediaplayer.route) {
+ MediaplayerPage()
+ }
+ }
+
navigation(
startDestination = Route.UtilitiesNavigator.TagEditor.route,
route = Route.UtilitiesNavigator.route
diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/common/Route.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/common/Route.kt
index 8cf6a95..f1247ae 100644
--- a/app/src/main/java/com/bobbyesp/metadator/presentation/common/Route.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/presentation/common/Route.kt
@@ -7,6 +7,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Handyman
import androidx.compose.material.icons.rounded.Home
+import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.ui.graphics.vector.ImageVector
import com.bobbyesp.metadator.App.Companion.json
import com.bobbyesp.metadator.R
@@ -35,6 +36,20 @@ sealed class Route(
}
}
+ data object MediaplayerNavigator : Route(
+ "mediaplayer_navigator",
+ title = R.string.mediaplayer,
+ icon = Icons.Rounded.PlayArrow
+ ) {
+ data object Mediaplayer :
+ Route(
+ "mediaplayer",
+ title = R.string.mediaplayer,
+ icon = Icons.Rounded.PlayArrow
+ ) {
+ }
+ }
+
data object UtilitiesNavigator :
Route(
"utilities",
@@ -61,6 +76,7 @@ sealed class Route(
val routesToNavigate = listOf(
Route.MetadatorNavigator,
+ Route.MediaplayerNavigator
)
fun Route.getTitle(@ApplicationContext context: Context): String? {
diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/components/cards/songs/HorizontalSongCard.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/components/cards/songs/HorizontalSongCard.kt
index 32fe2d2..22d7ad3 100644
--- a/app/src/main/java/com/bobbyesp/metadator/presentation/components/cards/songs/HorizontalSongCard.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/presentation/components/cards/songs/HorizontalSongCard.kt
@@ -1,6 +1,7 @@
package com.bobbyesp.metadator.presentation.components.cards.songs
import android.content.res.Configuration
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -20,6 +22,7 @@ import com.bobbyesp.metadator.presentation.components.image.ArtworkAsyncImage
import com.bobbyesp.metadator.presentation.theme.MetadatorTheme
import com.bobbyesp.model.Song
import com.bobbyesp.ui.components.text.MarqueeText
+import com.bobbyesp.utilities.Time
@Composable
fun HorizontalSongCard(
@@ -43,7 +46,9 @@ fun HorizontalSongCard(
artworkPath = song.artworkPath
)
Column(
- horizontalAlignment = Alignment.Start, modifier = Modifier.padding(8.dp)
+ horizontalAlignment = Alignment.Start, modifier = Modifier
+ .padding(vertical = 8.dp, horizontal = 6.dp)
+ .weight(1f)
) {
MarqueeText(
text = song.title,
@@ -59,6 +64,19 @@ fun HorizontalSongCard(
fontSize = 12.sp
)
}
+
+ Text(
+ text = Time.formatDuration(song.duration.toLong()),
+ style = MaterialTheme.typography.bodySmall,
+ fontSize = 12.sp,
+ modifier = Modifier
+ .padding(8.dp)
+ .background(
+ MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.1f),
+ MaterialTheme.shapes.small
+ )
+ .padding(6.dp)
+ )
}
}
}
diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerConstants.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerConstants.kt
new file mode 100644
index 0000000..cbb6694
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerConstants.kt
@@ -0,0 +1,10 @@
+package com.bobbyesp.metadator.presentation.components.others
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+val CollapsedPlayerHeight = 64.dp
+
+val PlayerAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow)
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt
new file mode 100644
index 0000000..a24c95e
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt
@@ -0,0 +1,22 @@
+package com.bobbyesp.metadator.presentation.components.others
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBarDefaults
+import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheet
+import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheetState
+
+@Composable
+fun MediaplayerSheet(modifier: Modifier = Modifier, state: DraggableBottomSheetState) {
+ DraggableBottomSheet(
+ state = state,
+ collapsedContent = {
+
+ },
+ backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(NavigationBarDefaults.Elevation)
+ ) {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt
index 4b5c536..a096ace 100644
--- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt
@@ -47,7 +47,7 @@ class MediaStorePageViewModel @Inject constructor(
val songs = withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
async {
- MediaStoreReceiver.getAllSongsFromMediaStore(
+ MediaStoreReceiver.getSongsBySearchTerm(
applicationContext = context,
)
}.await()
@@ -70,7 +70,7 @@ class MediaStorePageViewModel @Inject constructor(
) {
val songs = withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
async {
- MediaStoreReceiver.getAllSongsFromMediaStore(
+ MediaStoreReceiver.getSongsBySearchTerm(
applicationContext = context,
)
}.await()
@@ -95,7 +95,7 @@ class MediaStorePageViewModel @Inject constructor(
val songs = withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
async {
- MediaStoreReceiver.getAllSongsFromMediaStore(
+ MediaStoreReceiver.getSongsBySearchTerm(
applicationContext = context, searchTerm = filter, filterType = filterType
)
}.await()
diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt
index 21f658f..8a5bc95 100644
--- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt
@@ -35,7 +35,6 @@ import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -97,8 +96,6 @@ fun HomePage(
mutableStateOf(false)
}
- val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
-
val mediaStoreLazyGridState = rememberForeverLazyGridState(key = "lazyGrid")
val mediaStoreLazyColumnState = rememberLazyListState()
val pullState = rememberPullState()
@@ -115,7 +112,8 @@ fun HomePage(
val listIsFirstItemVisible by remember { derivedStateOf { mediaStoreLazyColumnState.firstVisibleItemIndex == 0 } }
Scaffold(
- modifier = modifier.fillMaxSize(), topBar = {
+ modifier = modifier.fillMaxSize(),
+ topBar = {
CenterAlignedTopAppBar(navigationIcon = {
IconButton(onClick = {
scope.launch {
@@ -175,7 +173,7 @@ fun HomePage(
})
}
}
- }, scrollBehavior = scrollBehavior
+ }
)
}, floatingActionButton = {
when (desiredLayout) {
diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerPage.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerPage.kt
new file mode 100644
index 0000000..54af986
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerPage.kt
@@ -0,0 +1,51 @@
+package com.bobbyesp.metadator.presentation.pages.mediaplayer
+
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.bobbyesp.metadator.presentation.components.others.CollapsedPlayerHeight
+import com.bobbyesp.metadator.presentation.components.others.MediaplayerSheet
+import com.bobbyesp.metadator.presentation.components.others.PlayerAnimationSpec
+import com.bobbyesp.ui.components.bottomsheet.draggable.rememberDraggableBottomSheetState
+
+@Composable
+fun MediaplayerPage() {
+ BoxWithConstraints(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ val mediaPlayerSheetState = rememberDraggableBottomSheetState(
+ dismissedBound = 0.dp,
+ collapsedBound = CollapsedPlayerHeight,
+ expandedBound = maxHeight,
+ animationSpec = PlayerAnimationSpec,
+ )
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ ) {
+ val scope = rememberCoroutineScope()
+
+ Button(onClick = { mediaPlayerSheetState.expandSoft() }) {
+ Text(text = "Expand soft")
+ }
+ }
+ }
+
+ MediaplayerSheet(
+ state = mediaPlayerSheetState
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a3016f8..b481c7e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -44,5 +44,6 @@
Warning
Unsaved changes
There are some metadata fields that has been changed and not saved. Do you really want to discard the changes?
+ Mediaplayer
\ No newline at end of file
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt
index 6b37b34..4035d10 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt
@@ -51,7 +51,7 @@ import kotlinx.coroutines.launch
/**
* Bottom Sheet
- * Modified from [ViMusic](https://github.com/vfsfitvnm/ViMusic)
+ * Improved version from [ViMusic](https://github.com/vfsfitvnm/ViMusic)
*/
@Composable
fun DraggableBottomSheet(
diff --git a/app/utilities/src/main/java/com/bobbyesp/utilities/Time.kt b/app/utilities/src/main/java/com/bobbyesp/utilities/Time.kt
index a0e815d..b7ec641 100644
--- a/app/utilities/src/main/java/com/bobbyesp/utilities/Time.kt
+++ b/app/utilities/src/main/java/com/bobbyesp/utilities/Time.kt
@@ -70,7 +70,7 @@ object Time {
fun formatDuration(duration: Long): String {
val minutes: Long = duration / 60000
val seconds: Long = (duration % 60000) / 1000
- return String.format("%02d:%02d", minutes, seconds)
+ return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
}
fun parseDateStringToLocalTime(dateString: String): String? {
diff --git a/app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/MediaStoreReceiver.kt b/app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/MediaStoreReceiver.kt
index 73b25b8..e067ab9 100644
--- a/app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/MediaStoreReceiver.kt
+++ b/app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/MediaStoreReceiver.kt
@@ -22,7 +22,7 @@ object MediaStoreReceiver {
* @param applicationContext The application context.
* @return A list of all the songs in the device.
*/
- fun getAllSongsFromMediaStore(applicationContext: Context): List {
+ fun getSongsBySearchTerm(applicationContext: Context): List {
val contentResolver: ContentResolver = applicationContext.contentResolver
val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
@@ -72,7 +72,7 @@ object MediaStoreReceiver {
return songs
}
- fun getAllSongsFromMediaStore(
+ fun getSongsBySearchTerm(
applicationContext: Context, searchTerm: String?, filterType: MediaStoreFilterType?
): List {
val contentResolver: ContentResolver = applicationContext.contentResolver