diff --git a/.gitignore b/.gitignore index a09ae0ffee..13952e45d6 100755 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,11 @@ app/src/main/java/com/metrolist/music/listentogether/proto/* # FFTW third-party library build artifacts .build-fftw app/src/main/cpp/coverart/ + +# Agents dir +.claude +.antigravity* +.gemini +.opencode +.cursor* +.codeium diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6b3e3f18aa..effb25eb64 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,7 @@ android:name=".MainActivity" android:exported="true" android:launchMode="singleTask" + android:resizeableActivity="true" android:theme="@style/Theme.Metrolist" android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/kotlin/com/metrolist/music/MainActivity.kt b/app/src/main/kotlin/com/metrolist/music/MainActivity.kt index 70948148b7..6837f49d71 100644 --- a/app/src/main/kotlin/com/metrolist/music/MainActivity.kt +++ b/app/src/main/kotlin/com/metrolist/music/MainActivity.kt @@ -85,6 +85,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalWindowInfo @@ -92,6 +93,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny import androidx.compose.ui.window.Dialog @@ -641,6 +643,26 @@ class MainActivity : ComponentActivity() { pureBlack = pureBlack, themeColor = themeColor, ) { + val currentDensity = LocalDensity.current + val windowInfo = LocalWindowInfo.current + val containerWidthDp = windowInfo.containerDpSize.width + + val densityScale = remember(containerWidthDp) { + when { + containerWidthDp >= 840.dp -> 1.25f + containerWidthDp >= 720.dp -> 1.15f + containerWidthDp >= 600.dp -> 1.1f + else -> 1.0f + } + } + val scaledDensity: Density = remember(currentDensity, densityScale) { + Density( + density = currentDensity.density * densityScale, + fontScale = currentDensity.fontScale, + ) + } + + CompositionLocalProvider(LocalDensity provides scaledDensity) { BoxWithConstraints( modifier = Modifier @@ -756,8 +778,9 @@ class MainActivity : ComponentActivity() { } val isLandscape = configuration.containerDpSize.width > configuration.containerDpSize.height + val isTablet = configuration.containerDpSize.width >= 600.dp - val showRail = isLandscape && !inSearchScreen + val showRail = (isLandscape || isTablet) && !inSearchScreen val navPadding = if (shouldShowNavigationBar && !showRail) { @@ -1367,6 +1390,7 @@ class MainActivity : ComponentActivity() { } } } + } } } diff --git a/app/src/main/kotlin/com/metrolist/music/db/MusicDatabase.kt b/app/src/main/kotlin/com/metrolist/music/db/MusicDatabase.kt index d73d34e227..a344740d2f 100644 --- a/app/src/main/kotlin/com/metrolist/music/db/MusicDatabase.kt +++ b/app/src/main/kotlin/com/metrolist/music/db/MusicDatabase.kt @@ -54,7 +54,7 @@ import java.util.Date import java.util.Locale class MusicDatabase( - private val delegate: InternalDatabase, + val delegate: InternalDatabase, ) : DatabaseDao by delegate.dao { val speedDialDao: SpeedDialDao get() = delegate.speedDialDao @@ -177,6 +177,7 @@ abstract class InternalDatabase : RoomDatabase() { MIGRATION_21_24, MIGRATION_22_24, MIGRATION_24_25, + MIGRATION_38_37, ).fallbackToDestructiveMigration(dropAllTables = true) .setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING) .setTransactionExecutor( @@ -813,6 +814,21 @@ val MIGRATION_24_25 = } } +val MIGRATION_38_37 = + object : Migration(38, 37) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS `ArtistPageCache`") + db.execSQL( + "CREATE TABLE IF NOT EXISTS `artist_new` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT 0, `isPodcastChannel` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))" + ) + db.execSQL( + "INSERT INTO `artist_new` (`id`, `name`, `thumbnailUrl`, `channelId`, `lastUpdateTime`, `bookmarkedAt`, `isLocal`, `isPodcastChannel`) SELECT `id`, `name`, `thumbnailUrl`, `channelId`, `lastUpdateTime`, `bookmarkedAt`, `isLocal`, `isPodcastChannel` FROM `artist`" + ) + db.execSQL("DROP TABLE `artist`") + db.execSQL("ALTER TABLE `artist_new` RENAME TO `artist`") + } + } + class Migration29To30 : AutoMigrationSpec { override fun onPostMigrate(db: SupportSQLiteDatabase) { // Ensure isVideo column exists (safeguard) diff --git a/app/src/main/kotlin/com/metrolist/music/di/AppModule.kt b/app/src/main/kotlin/com/metrolist/music/di/AppModule.kt index f0fe16e9b1..b711d78f1e 100644 --- a/app/src/main/kotlin/com/metrolist/music/di/AppModule.kt +++ b/app/src/main/kotlin/com/metrolist/music/di/AppModule.kt @@ -48,17 +48,15 @@ object AppModule { @Singleton @Provides - fun provideInternalDatabase( + fun provideDatabase( @ApplicationContext context: Context, - ): InternalDatabase = Room - .databaseBuilder(context, InternalDatabase::class.java, InternalDatabase.DB_NAME) - .build() + ): MusicDatabase = InternalDatabase.newInstance(context) @Singleton @Provides - fun provideDatabase( - internalDatabase: InternalDatabase, - ): MusicDatabase = MusicDatabase(internalDatabase) + fun provideInternalDatabase( + database: MusicDatabase, + ): InternalDatabase = database.delegate @Singleton @Provides diff --git a/innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt b/innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt index cdbfa38dff..b3a8504018 100644 --- a/innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt +++ b/innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt @@ -1000,6 +1000,11 @@ object YouTube { if (name != null) return@run Artist(name = name, id = browseId) } + val fromMusicHeaderStrapline = response.header?.musicHeaderRenderer + ?.straplineTextOne?.runs?.firstOrNull() + ?.let { Artist(name = it.text, id = it.navigationEndpoint?.browseEndpoint?.browseId) } + if (fromMusicHeaderStrapline != null) return@run fromMusicHeaderStrapline + null } @@ -1013,14 +1018,13 @@ object YouTube { PlaylistItem( id = playlistId, title = - header - ?.title - ?.runs - ?.firstOrNull() - ?.text!!, + header?.title?.runs?.firstOrNull()?.text + ?: response.header?.musicHeaderRenderer?.title?.runs?.firstOrNull()?.text + ?: "", author = author, songCountText = - header.secondSubtitle + (header?.secondSubtitle + ?: response.header?.musicHeaderRenderer?.secondSubtitle) ?.runs ?.findLast { it.text.any { c -> c.isDigit() } && @@ -1029,68 +1033,90 @@ object YouTube { !it.text.contains("minute", ignoreCase = true) }?.text, thumbnail = - header.thumbnail + header?.thumbnail ?.musicThumbnailRenderer ?.thumbnail ?.thumbnails ?.lastOrNull() - ?.url!!, + ?.url + ?: response.header + ?.musicHeaderRenderer + ?.thumbnail + ?.musicThumbnailRenderer + ?.thumbnails + ?.lastOrNull() + ?.url + ?: "", playEndpoint = null, shuffleEndpoint = - header.buttons - .lastOrNull() - ?.menuRenderer - ?.items - ?.firstOrNull() - ?.menuNavigationItemRenderer - ?.navigationEndpoint - ?.watchPlaylistEndpoint!!, + header?.buttons?.lastOrNull() + ?.menuRenderer?.items?.firstOrNull() + ?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint + ?: response.header?.musicHeaderRenderer?.buttons?.lastOrNull() + ?.menuRenderer?.items?.firstOrNull() + ?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint, radioEndpoint = - header.buttons - .getOrNull(2) - ?.menuRenderer - ?.items - ?.find { + header?.buttons?.getOrNull(2) + ?.menuRenderer?.items?.find { it.menuNavigationItemRenderer?.icon?.iconType == "MIX" - }?.menuNavigationItemRenderer - ?.navigationEndpoint - ?.watchPlaylistEndpoint, + }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint + ?: response.header?.musicHeaderRenderer?.buttons?.getOrNull(2) + ?.menuRenderer?.items?.find { + it.menuNavigationItemRenderer?.icon?.iconType == "MIX" + }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint, isEditable = editable, description = description, authorAvatarUrl = authorAvatarUrl, ), - songs = - response.contents - ?.twoColumnBrowseResultsRenderer - ?.secondaryContents - ?.sectionListRenderer - ?.contents - ?.firstOrNull() - ?.musicPlaylistShelfRenderer + songs = run { + val twoColShelf = + response.contents + ?.twoColumnBrowseResultsRenderer + ?.secondaryContents + ?.sectionListRenderer + ?.contents + ?.firstOrNull() + ?.musicPlaylistShelfRenderer + val singleColShelf = + response.contents + ?.singleColumnBrowseResultsRenderer + ?.tabs + ?.firstOrNull() + ?.tabRenderer + ?.content + ?.sectionListRenderer + ?.contents + ?.firstOrNull() + ?.musicPlaylistShelfRenderer + (twoColShelf ?: singleColShelf) ?.contents ?.getItems() - ?.mapNotNull { - PlaylistPage.fromMusicResponsiveListItemRenderer(it) - } ?: emptyList(), - songsContinuation = - response.contents - ?.twoColumnBrowseResultsRenderer - ?.secondaryContents - ?.sectionListRenderer - ?.contents - ?.firstOrNull() - ?.musicPlaylistShelfRenderer - ?.contents - ?.getContinuation() - ?: response.contents + ?.mapNotNull { PlaylistPage.fromMusicResponsiveListItemRenderer(it) } + ?: emptyList() + }, + songsContinuation = run { + val twoColShelf = + response.contents ?.twoColumnBrowseResultsRenderer ?.secondaryContents ?.sectionListRenderer ?.contents ?.firstOrNull() ?.musicPlaylistShelfRenderer - ?.continuations - ?.getContinuation(), + val singleColShelf = + response.contents + ?.singleColumnBrowseResultsRenderer + ?.tabs + ?.firstOrNull() + ?.tabRenderer + ?.content + ?.sectionListRenderer + ?.contents + ?.firstOrNull() + ?.musicPlaylistShelfRenderer + val shelf = twoColShelf ?: singleColShelf + shelf?.contents?.getContinuation() ?: shelf?.continuations?.getContinuation() + }, continuation = response.contents ?.twoColumnBrowseResultsRenderer