Compare commits

10 Commits

Author SHA1 Message Date
acite
756c2ea9f8 [update] Landspace player top bar padding 2025-09-23 13:25:24 +08:00
acite
c9c3306766 [update] New full screen switching mechanism 2025-09-23 13:21:51 +08:00
acite
49751c55d9 [feat] Video Player lock 2025-09-23 02:53:42 +08:00
acite
d918508c16 [fix] Video Remember id mass 2025-09-22 13:15:16 +08:00
acite
d858cd18bd [update] Video Card Font 2025-09-21 01:34:01 +08:00
acite
82f537038c [feat] Show comic id 2025-09-20 14:28:05 +08:00
acite
a298cb75e2 [fix] Crash when page initialized before view model 2025-09-20 14:16:46 +08:00
acite
92f0e8543e [fix] Crash while empty class set 2025-09-20 13:38:46 +08:00
acite
f78bcc83c9 [fix] Vtt ext name 2025-09-20 13:11:24 +08:00
acite
55ea2e1ae3 [feat] Video system optimization2 2025-09-20 03:18:25 +08:00
11 changed files with 800 additions and 391 deletions

View File

@@ -51,6 +51,7 @@ dependencies {
implementation(libs.hilt.android) implementation(libs.hilt.android)
implementation(libs.hilt.navigation.compose) implementation(libs.hilt.navigation.compose)
implementation(libs.androidx.compose.material.core)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
@@ -61,6 +62,7 @@ dependencies {
implementation(libs.bcprov.jdk15on) implementation(libs.bcprov.jdk15on)
implementation(libs.converter.gson) implementation(libs.converter.gson)
implementation(libs.gson) implementation(libs.gson)
implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.common) implementation(libs.androidx.media3.common)

View File

@@ -28,6 +28,13 @@ class Video(
"${ApiClient.getBase()}api/video/$klass/$id/av?token=$token" "${ApiClient.getBase()}api/video/$klass/$id/av?token=$token"
} }
fun getSubtitle(): String {
return if (isLocal)
"$localBase/videos/$klass/$id/subtitle.vtt"
else
"${ApiClient.getBase()}api/video/$klass/$id/subtitle?token=$token"
}
fun getGallery(): List<KeyImage> { fun getGallery(): List<KeyImage> {
return if (isLocal) return if (isLocal)
video.gallery.map { video.gallery.map {

View File

@@ -22,6 +22,6 @@ interface VideoRecordDao {
@Delete @Delete
suspend fun delete(rec: VideoRecord) suspend fun delete(rec: VideoRecord)
@Query("SELECT * FROM videorecord WHERE id = :id") @Query("SELECT * FROM videorecord WHERE id = :id and name = :klass")
suspend fun getById(id: String): VideoRecord? suspend fun get(id: String, klass: String): VideoRecord?
} }

View File

@@ -157,6 +157,11 @@ class FetchManager @Inject constructor(
video.getCover(), video.getCover(),
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/cover.jpg")) File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/cover.jpg"))
downloadFile(
client!!,
video.getSubtitle(),
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/subtitle.vtt"))
enqueue(request) enqueue(request)
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/summary.json").writeText(Json.encodeToString(video)) File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/summary.json").writeText(Json.encodeToString(video))

View File

@@ -130,8 +130,8 @@ fun generateColorScheme(primaryColor: Color, isDarkMode: Boolean): ColorScheme {
} }
} }
private val DarkColorScheme = generateColorScheme(Color(0xFF4A6F9F), isDarkMode = true) private val DarkColorScheme = generateColorScheme(Color(0xFF36A0FF), isDarkMode = true)
private val LightColorScheme = generateColorScheme(Color(0xFF4A6F9F), isDarkMode = false) private val LightColorScheme = generateColorScheme(Color(0xFF36A0FF), isDarkMode = false)
@Composable @Composable
fun AetherTheme( fun AetherTheme(

View File

@@ -187,7 +187,7 @@ fun ComicScreen(
HorizontalDivider(thickness = 1.5.dp) HorizontalDivider(thickness = 1.5.dp)
LazyVerticalStaggeredGrid( LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(128.dp), columns = StaggeredGridCells.Adaptive(136.dp),
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(8.dp),
verticalItemSpacing = 8.dp, verticalItemSpacing = 8.dp,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -277,9 +277,18 @@ fun ComicCard(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
maxLines = 2, maxLines = 2,
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(4.dp)
.background(Color.Transparent)
.heightIn(max = 48.dp)
)
Spacer(Modifier.height(4.dp))
Text(
text = "Id: ${comic.id}",
fontSize = 12.sp,
maxLines = 2,
modifier = Modifier
.padding(bottom = 4.dp).padding(horizontal = 4.dp)
.background(Color.Transparent) .background(Color.Transparent)
.heightIn(48.dp)
) )
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -118,7 +118,9 @@ fun VideoScreen(
val tabIndex by videoScreenViewModel.tabIndex val tabIndex by videoScreenViewModel.tabIndex
var menuVisibility by videoScreenViewModel.menuVisibility var menuVisibility by videoScreenViewModel.menuVisibility
var searchFilter by videoScreenViewModel.searchFilter var searchFilter by videoScreenViewModel.searchFilter
var doneInit by videoScreenViewModel.doneInit
if (doneInit)
CardPage(title = "Videos") { CardPage(title = "Videos") {
Box(Modifier.fillMaxSize()) Box(Modifier.fillMaxSize())
{ {
@@ -165,7 +167,9 @@ fun VideoScreen(
Box(Modifier.fillMaxHeight()) Box(Modifier.fillMaxHeight())
{ {
Text( Text(
text = videoScreenViewModel.videoLibrary.classes.getOrNull(tabIndex) text = videoScreenViewModel.videoLibrary.classes.getOrNull(
tabIndex
)
?: "", ?: "",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -179,13 +183,15 @@ fun VideoScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.height(36.dp).widthIn(max = 240.dp) .height(36.dp)
.widthIn(max = 240.dp)
.background(colorScheme.primary, RoundedCornerShape(8.dp)) .background(colorScheme.primary, RoundedCornerShape(8.dp))
.padding(horizontal = 6.dp) .padding(horizontal = 6.dp)
) { ) {
Icon( Icon(
modifier = Modifier modifier = Modifier
.size(30.dp).align(Alignment.CenterVertically), .size(30.dp)
.align(Alignment.CenterVertically),
imageVector = Icons.Default.Search, imageVector = Icons.Default.Search,
contentDescription = "Catalogue" contentDescription = "Catalogue"
) )
@@ -203,12 +209,18 @@ fun VideoScreen(
) )
} }
} }
HorizontalDivider(Modifier.padding(bottom = 8.dp), 1.5.dp, DividerDefaults.color) HorizontalDivider(
Modifier.padding(bottom = 8.dp),
1.5.dp,
DividerDefaults.color
)
LazyVerticalStaggeredGrid( LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(160.dp), columns = StaggeredGridCells.Adaptive(160.dp),
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(8.dp),
verticalItemSpacing = 8.dp, verticalItemSpacing = 8.dp,
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp), horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(
8.dp
),
state = state, state = state,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -240,7 +252,7 @@ fun VideoScreen(
Card( Card(
Modifier Modifier
.fillMaxHeight() .fillMaxHeight()
.width(200.dp) .width(250.dp)
.align(Alignment.CenterEnd), .align(Alignment.CenterEnd),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.surface) colors = CardDefaults.cardColors(containerColor = colorScheme.surface)
@@ -282,13 +294,13 @@ fun CatalogueItemRow(
.padding(4.dp) .padding(4.dp)
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.heightIn(min = 28.dp) .heightIn(min = 28.dp)
.width(200.dp), .width(250.dp),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.primary) colors = CardDefaults.cardColors(containerColor = colorScheme.primary)
) { ) {
Text( Text(
text = item.second, text = item.second,
fontSize = 18.sp, fontSize = 14.sp,
maxLines = 1, maxLines = 1,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
@@ -298,27 +310,6 @@ fun CatalogueItemRow(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopRow(videoScreenViewModel: VideoScreenViewModel) {
val tabIndex by videoScreenViewModel.tabIndex;
if (videoScreenViewModel.videoLibrary.classes.isEmpty()) return
val colorScheme = MaterialTheme.colorScheme
ScrollableTabRow(
selectedTabIndex = tabIndex,
modifier = Modifier.background(colorScheme.surface)
) {
videoScreenViewModel.videoLibrary.classes.forEachIndexed { index, title ->
Tab(
selected = tabIndex == index,
onClick = { videoScreenViewModel.setTabIndex(index) },
text = { Text(text = title, maxLines = 1) },
)
}
}
}
@Composable @Composable
fun VideoCard( fun VideoCard(
video: Video, video: Video,
@@ -354,7 +345,7 @@ fun VideoCard(
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth(),
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
@@ -415,21 +406,30 @@ fun VideoCard(
} }
Text( Text(
text = video.video.name, text = video.video.name,
fontSize = 14.sp, fontSize = 12.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
maxLines = 2, maxLines = 4,
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.background(Color.Transparent) .background(Color.Transparent)
.heightIn(24.dp) .heightIn(min = 24.dp),
lineHeight = 14.sp
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Row( Row(
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text("Class: ", fontSize = 12.sp, maxLines = 1) Text("Class: ", fontSize = 10.sp, maxLines = 1)
Text(video.klass, fontSize = 12.sp, maxLines = 1) Text(video.klass, fontSize = 10.sp, maxLines = 1)
}
Row(
modifier = Modifier.padding(horizontal = 8.dp).padding(bottom = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("Id: ", fontSize = 10.sp, maxLines = 1, lineHeight = 10.sp)
Text(video.id, fontSize = 10.sp, maxLines = 1, lineHeight = 10.sp)
} }
} }
} }

View File

@@ -18,6 +18,7 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_READY import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.text.Cue
import androidx.media3.common.util.Log import androidx.media3.common.util.Log
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
@@ -40,8 +41,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Request
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import androidx.core.net.toUri
import androidx.media3.common.C
import androidx.media3.common.Tracks
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
@HiltViewModel @HiltViewModel
class VideoPlayerViewModel @Inject constructor( class VideoPlayerViewModel @Inject constructor(
@@ -64,7 +71,7 @@ class VideoPlayerViewModel @Inject constructor(
var thumbUp by mutableIntStateOf(0) var thumbUp by mutableIntStateOf(0)
var thumbDown by mutableIntStateOf(0) var thumbDown by mutableIntStateOf(0)
var star by mutableStateOf(false) var star by mutableStateOf(false)
var locked by mutableStateOf(false)
private var _init: Boolean = false; private var _init: Boolean = false;
var startPlaying by mutableStateOf(false) var startPlaying by mutableStateOf(false)
var renderedFirst = false var renderedFirst = false
@@ -74,6 +81,7 @@ class VideoPlayerViewModel @Inject constructor(
var imageLoader: ImageLoader? = null; var imageLoader: ImageLoader? = null;
var brit by mutableFloatStateOf(0.5f) var brit by mutableFloatStateOf(0.5f)
val database: VideoRecordDatabase = VideoRecordDatabase.getDatabase(context) val database: VideoRecordDatabase = VideoRecordDatabase.getDatabase(context)
var cues by mutableStateOf(listOf<Cue>())
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun init(videoId: String) { fun init(videoId: String) {
@@ -88,21 +96,57 @@ class VideoPlayerViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
video = mediaManager.queryVideo(v.split("/")[0], v.split("/")[1])!! video = mediaManager.queryVideo(v.split("/")[0], v.split("/")[1])!!
recentManager.pushVideo(context, VideoQueryIndex(v.split("/")[0], v.split("/")[1])) recentManager.pushVideo(context, VideoQueryIndex(v.split("/")[0], v.split("/")[1]))
_player =
(if (video!!.isLocal) ExoPlayer.Builder(context) else ExoPlayer.Builder(context) val subtitleCandidate = video?.getSubtitle()?.trim()
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))) val subtitleUri = tryResolveSubtitleUri(subtitleCandidate)
.build().apply {
val url = video?.getVideo() ?: "" // decide whether we need network-capable media source factory:
val mediaItem = if (video!!.isLocal) val subtitleIsRemote = subtitleUri?.scheme?.startsWith("http", ignoreCase = true) == true
MediaItem.fromUri(Uri.fromFile(File(url))) val videoIsRemote = !video!!.isLocal
val needNetworkFactory = videoIsRemote || subtitleIsRemote
val trackSelector = DefaultTrackSelector(context)
// build ExoPlayer with or without custom DefaultMediaSourceFactory
val builder = if (needNetworkFactory)
ExoPlayer.Builder(context).setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
else else
MediaItem.fromUri(url) ExoPlayer.Builder(context)
_player = builder.setTrackSelector(trackSelector).build().apply {
val url = video?.getVideo() ?: ""
val videoUri = if (video!!.isLocal) Uri.fromFile(File(url)) else url.toUri()
val mediaItem: MediaItem = if (subtitleUri != null) {
// prepare subtitle configuration with guessed mime type
val subConfig = MediaItem.SubtitleConfiguration.Builder(subtitleUri)
.setMimeType("text/vtt")
.build()
MediaItem.Builder()
.setUri(videoUri)
.setSubtitleConfigurations(listOf(subConfig))
.build()
} else {
MediaItem.fromUri(videoUri)
}
setMediaItem(mediaItem) setMediaItem(mediaItem)
prepare() prepare()
playWhenReady = true playWhenReady = true
addListener(object : Player.Listener { addListener(object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) {
super.onTracksChanged(tracks)
val trackSelector = _player?.trackSelector
if (trackSelector is DefaultTrackSelector) {
val parameters = trackSelector.buildUponParameters()
.setSelectUndeterminedTextLanguage(true)
.build()
trackSelector.parameters = parameters
}
}
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == STATE_READY) { if (playbackState == STATE_READY) {
startPlaying = true startPlaying = true
@@ -114,7 +158,7 @@ class VideoPlayerViewModel @Inject constructor(
if(!renderedFirst) if(!renderedFirst)
{ {
viewModelScope.launch { viewModelScope.launch {
val ii = database.userDao().getById(video!!.id) val ii = database.userDao().get(video!!.id, video!!.klass)
if(ii != null) if(ii != null)
{ {
_player!!.seekTo(ii.position) _player!!.seekTo(ii.position)
@@ -125,8 +169,13 @@ class VideoPlayerViewModel @Inject constructor(
renderedFirst = true renderedFirst = true
} }
override fun onPlayerError(error: PlaybackException) { override fun onPlayerError(error: PlaybackException)
{
print(error.message)
}
override fun onCues(lcues: MutableList<Cue>) {
cues = lcues
} }
}) })
} }
@@ -135,6 +184,66 @@ class VideoPlayerViewModel @Inject constructor(
_init = true; _init = true;
} }
/**
* Try to resolve the given subtitle pathOrUrl to a Uri.
* - If it's a local path and file exists -> Uri.fromFile
* - If it's a http(s) URL -> try HEAD; if HEAD unsupported, try GET with Range: bytes=0-1
* - Return null when unreachable / 404 / not exist
*/
private suspend fun tryResolveSubtitleUri(pathOrUrl: String?): Uri? = withContext(Dispatchers.IO) {
if (pathOrUrl.isNullOrBlank()) return@withContext null
val trimmed = pathOrUrl.trim()
// Remote URL case (http/https)
if (trimmed.startsWith("http://", ignoreCase = true) || trimmed.startsWith("https://", ignoreCase = true)) {
try {
val client = createOkHttp()
var headReq = Request.Builder().url(trimmed).head().build()
var headResp = try { client.newCall(headReq).execute() } catch (e: Exception) { null }
headResp?.use { resp ->
val code = resp.code
if (code == 200 || code == 206) {
return@withContext trimmed.toUri()
}
if (code == 404) {
return@withContext null
}
}
val rangeReq = Request.Builder()
.url(trimmed)
.addHeader("Range", "bytes=0-1")
.get()
.build()
var rangeResp = try { client.newCall(rangeReq).execute() } catch (e: Exception) { null }
rangeResp?.use { resp ->
val code = resp.code
if (code == 206) {
return@withContext trimmed.toUri()
}
if (code == 200) {
return@withContext trimmed.toUri()
}
if (code == 404) {
return@withContext null
}
}
} catch (e: Exception) {
return@withContext null
}
return@withContext null
} else {
// Local path
val f = File(trimmed)
return@withContext if (f.exists() && f.isFile) Uri.fromFile(f) else null
}
}
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun startListen() { fun startListen() {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {

View File

@@ -47,12 +47,16 @@ class VideoScreenViewModel @Inject constructor(
var imageLoader: ImageLoader? = null; var imageLoader: ImageLoader? = null;
var menuVisibility = mutableStateOf(false) var menuVisibility = mutableStateOf(false)
var searchFilter = mutableStateOf("") var searchFilter = mutableStateOf("")
var doneInit = mutableStateOf(false)
suspend fun init() { suspend fun init() {
fetchManager.configured.filter { it }.first() fetchManager.configured.filter { it }.first()
if (Global.loggedIn) { if (Global.loggedIn) {
videoLibrary.classes.addAll(mediaManager.listVideoKlasses()) videoLibrary.classes.addAll(mediaManager.listVideoKlasses())
if(videoLibrary.classes.isEmpty())
return
var i = 0 var i = 0
for (it in videoLibrary.classes) { for (it in videoLibrary.classes) {
videoLibrary.updatingMap[i++] = false videoLibrary.updatingMap[i++] = false
@@ -66,7 +70,8 @@ class VideoScreenViewModel @Inject constructor(
val r = vl.sortedWith(compareBy(naturalOrder()) { it.video.name }) val r = vl.sortedWith(compareBy(naturalOrder()) { it.video.name })
videoLibrary.classesMap[videoLibrary.classes[0]]?.addAll(r) videoLibrary.classesMap[videoLibrary.classes[0]]?.addAll(r)
} }
} else { }
else {
videoLibrary.classes.add("Offline") videoLibrary.classes.add("Offline")
videoLibrary.updatingMap[0] = true videoLibrary.updatingMap[0] = true
videoLibrary.classesMap["Offline"] = mutableStateListOf<Video>() videoLibrary.classesMap["Offline"] = mutableStateListOf<Video>()
@@ -82,6 +87,8 @@ class VideoScreenViewModel @Inject constructor(
videoLibrary.classesMap[videoLibrary.classes[0]]?.addAll(jsonQuery) videoLibrary.classesMap[videoLibrary.classes[0]]?.addAll(jsonQuery)
} }
doneInit.value = true
} }
fun setTabIndex(index: Int) { fun setTabIndex(index: Int) {

View File

@@ -22,6 +22,7 @@ activityCompose = "1.11.0"
composeBom = "2025.09.00" composeBom = "2025.09.00"
media3Common = "1.8.0" media3Common = "1.8.0"
media3Exoplayer = "1.8.0" media3Exoplayer = "1.8.0"
media3ExoplayerFfmpeg = "1.8.0"
media3Ui = "1.8.0" media3Ui = "1.8.0"
navigationCompose = "2.9.4" navigationCompose = "2.9.4"
okhttp = "5.1.0" okhttp = "5.1.0"
@@ -35,6 +36,7 @@ roomRuntime = "2.8.0"
ksp = "2.1.21-2.0.2" ksp = "2.1.21-2.0.2"
hilt = "2.57.1" hilt = "2.57.1"
hilt-navigation-compose = "1.3.0" hilt-navigation-compose = "1.3.0"
composeMaterialCore = "1.5.1"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -77,6 +79,7 @@ androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } 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-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" } hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
androidx-compose-material-core = { group = "androidx.wear.compose", name = "compose-material-core", version.ref = "composeMaterialCore" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }