Skip to content

Fix landscape scaling by using smallest dimension for density scaling#3933

Open
kairosci wants to merge 2 commits into
MetrolistGroup:mainfrom
kairosci:fix-landscape-scaling
Open

Fix landscape scaling by using smallest dimension for density scaling#3933
kairosci wants to merge 2 commits into
MetrolistGroup:mainfrom
kairosci:fix-landscape-scaling

Conversation

@kairosci

@kairosci kairosci commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Problem

The UI was excessively zoomed in when switching to landscape orientation on phones, making elements like lyrics difficult to view. Additionally, rotation transitions were sluggish.

Cause

The adaptive density scaling logic in MainActivity.kt was based solely on containerWidthDp. On phones in landscape, the width often exceeded 600dp, triggering tablet-level scaling (1.1x+). This increased density reduced logical vertical space and forced a full UI recomposition during rotation because the density value changed.

Solution

Modified the scaling logic to use the smallest dimension (minOf(width, height)) for determining the scale factor. This makes the scaling orientation-independent (following the sw convention) and ensures consistent density across rotations.

Testing

Verified that phones (sw < 600dp) stay at 1.0x scale in both portrait and landscape. Large tablets still benefit from scaling as intended.

Related Issues

Summary by CodeRabbit

  • New Features
    • Added landscape scaling toggle in appearance settings. When enabled, the UI automatically adjusts density scaling based on screen dimensions for optimized layout on landscape-oriented devices. Users can toggle this feature on or off to suit their preferences.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3a29078-0fa4-412c-8843-e926fe272e28

📥 Commits

Reviewing files that changed from the base of the PR and between c86d0f8 and 3503b24.

📒 Files selected for processing (4)
  • app/src/main/kotlin/com/metrolist/music/MainActivity.kt
  • app/src/main/kotlin/com/metrolist/music/constants/PreferenceKeys.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt
  • app/src/main/res/values/metrolist_strings.xml
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/src/main/res/values/metrolist_strings.xml
  • app/src/main/kotlin/com/metrolist/music/MainActivity.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt
  • app/src/main/kotlin/com/metrolist/music/constants/PreferenceKeys.kt

📝 Walkthrough

Walkthrough

This PR adds a user-configurable landscape scaling feature that adapts UI density based on the smallest screen dimension. A new preference key gates density scaling behavior; when enabled, UI density scales by 1.15f/1.1f/1.05f/1.0f based on screen size thresholds, otherwise stays at 1.0f. The preference is wired into settings with a toggle switch, and corresponding string resources are added.

Changes

UI density scaling and settings

Layer / File(s) Summary
Preference key and settings UI wiring
app/src/main/kotlin/com/metrolist/music/constants/PreferenceKeys.kt, app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt, app/src/main/res/values/metrolist_strings.xml
EnableLandscapeScalingKey is defined as a boolean preference and wired into AppearanceSettings via rememberPreference (default false). A new theme-group settings row renders a switch that toggles the preference; row click inverts the value. Three string resources provide the setting label, description, and a "Dynamic Theme" label.
Density scaling computation and application
app/src/main/kotlin/com/metrolist/music/MainActivity.kt
MetrolistApp reads enableLandscapeScaling preference and computes smallestDimensionDp as min(width, height) from window container size. When enabled, density scale is mapped by thresholds (≥840 → 1.15f, ≥720 → 1.1f, ≥600 → 1.05f, else → 1.0f); when disabled, scale is fixed to 1.0f. Scaled Density is applied via CompositionLocalProvider(LocalDensity provides scaledDensity). Minor BoxWithConstraints formatting adjustment preserves modifier chain.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • MetrolistGroup/Metrolist#3793: Both PRs modify MainActivity's Compose LocalDensity scaling logic (tablet/landscape density adaptation via CompositionLocalProvider—this PR gates scaling behind a new preference and uses smallest-dimension thresholds, while the retrieved PR implements width-based automatic scaling).

Suggested reviewers

  • nyxiereal
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing landscape scaling by using the smallest dimension for density calculations.
Description check ✅ Passed The description follows the template structure with all required sections: Problem, Cause, Solution, Testing, and Related Issues are all present and complete.
Linked Issues check ✅ Passed The code changes directly address issue #3884's requirement by implementing orientation-independent scaling logic using the smallest dimension instead of container width.
Out of Scope Changes check ✅ Passed All changes are scoped to the landscape scaling fix: preference key addition, MainActivity density logic modification, UI settings toggle, and localized strings.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kairosci

kairosci commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

@Nash20718 @E021ntox I created this to resolve the issue. Could you please confirm that it closes the issue?
Download at https://github.com/MetrolistGroup/Metrolist/actions/runs/27072123335/artifacts/7457465804

@E021ntox

E021ntox commented Jun 6, 2026

Copy link
Copy Markdown

@kairosci I've tested this APK, and honestly, there's no difference from 13.5.0. I suggest not overcomplicating things here; the default 1.0 scaling already looks great in floating window, full-screen, and split-screen modes. Can you imagine a 13-inch screen showing only 6 rows of items in the settings menu?

Also, please consider this as well. Thank you! #3386 (comment)

@kairosci kairosci marked this pull request as draft June 7, 2026 09:06
@kairosci kairosci force-pushed the fix-landscape-scaling branch from b9fec12 to c86d0f8 Compare June 7, 2026 09:09
@kairosci kairosci marked this pull request as ready for review June 7, 2026 11:47
@kairosci kairosci force-pushed the fix-landscape-scaling branch from c86d0f8 to 758770c Compare June 7, 2026 11:49
@kairosci kairosci force-pushed the fix-landscape-scaling branch from 758770c to 3503b24 Compare June 7, 2026 11:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
strip_scaling.py (1)

3-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Script is incomplete and currently cannot perform any “strip scaling” transformation.

At Line [6], out_lines is never populated, and from Line [34] onward the flow ends in a placeholder pass with no output write, so running this file produces no usable result.

Proposed fix
 import re
+from pathlib import Path
 
-with open('app/src/main/kotlin/com/metrolist/music/MainActivity.kt', 'r') as f:
+target = Path("app/src/main/kotlin/com/metrolist/music/MainActivity.kt")
+with target.open("r", encoding="utf-8") as f:
     lines = f.readlines()
 
 out_lines = []
 in_provider = False
 provider_indent = -1
 skip_next = False
+skip_brace_depth = 0
+provider_brace_depth = 0
 
-for i, line in enumerate(lines):
+for line in lines:
+    if skip_brace_depth > 0:
+        skip_brace_depth += line.count("{") - line.count("}")
+        if skip_brace_depth <= 0:
+            skip_brace_depth = 0
+        continue
+
     if "val currentDensity = LocalDensity.current" in line:
         continue
@@
     if "val densityScale = remember(" in line:
-        skip_next = True
+        skip_brace_depth = max(1, line.count("{") - line.count("}"))
         continue
-    if skip_next:
-        if "}" in line and line.strip() == "}":
-            skip_next = False
-        continue
     if "val scaledDensity: Density = remember" in line:
-        skip_next = True
+        skip_brace_depth = max(1, line.count("{") - line.count("}"))
         continue
     if "CompositionLocalProvider(LocalDensity provides scaledDensity) {" in line:
         provider_indent = len(line) - len(line.lstrip())
+        in_provider = True
+        provider_brace_depth = 1
         continue
-
-    # We need to drop the matching closing brace for CompositionLocalProvider
-    # But since it's hard to track in a simple loop without a stack, maybe just use regex on the whole content.
-    pass
+    if in_provider:
+        provider_brace_depth += line.count("{") - line.count("}")
+        if provider_brace_depth <= 0:
+            in_provider = False
+        continue
+
+    out_lines.append(line)
+
+with target.open("w", encoding="utf-8") as f:
+    f.writelines(out_lines)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@strip_scaling.py` around lines 3 - 37, The script never builds or writes
out_lines and exits with a placeholder pass, so implement full transformation:
iterate through lines, append non-skipped lines to out_lines (use the existing
flags out_lines, in_provider, provider_indent, skip_next), detect and drop the
whole CompositionLocalProvider block (start when
"CompositionLocalProvider(LocalDensity provides scaledDensity) {" is seen and
skip until its matching closing brace using a brace counter or stack), skip the
remembered density-related lines (the checks for "val densityScale = remember(",
"val scaledDensity: Density = remember", etc.) as already started, and finally
write the joined out_lines back to the same file; ensure variables and
conditions (out_lines, in_provider, provider_indent, skip_next,
CompositionLocalProvider) are used to locate and remove the intended sections.
🧹 Nitpick comments (1)
diff_to_apply.patch (1)

428-475: 💤 Low value

Consider extracting duplicated artist/otherInfo extraction logic.

The artist extraction (when (item) { is SongItem -> ... }) and otherInfo computation are nearly identical between YouTubeListItem (lines 365-389) and YouTubeGridItem (lines 441-466). Extracting these into helper functions would reduce duplication and simplify future maintenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@diff_to_apply.patch` around lines 428 - 475, You have duplicated logic for
extracting artists and the "otherInfo" string in YouTubeGridItem that mirrors
code in YouTubeListItem; refactor by creating helper functions (e.g., fun
extractArtists(item: YouTubeItem): List<Artist>? and fun extractOtherInfo(item:
YouTubeItem): String?) and replace the inline when blocks in both
YouTubeGridItem and YouTubeListItem to call these helpers; ensure the helpers
handle SongItem, AlbumItem, PlaylistItem, PodcastItem, EpisodeItem cases
(matching the existing logic used for ClickableArtistText and the subtitle Text)
so behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@diff_to_apply.patch`:
- Around line 358-414: The badges call was moved inside the "if (item !is
ArtistItem)" branch so ArtistItem no longer renders badges; restore badge
rendering by invoking badges(this) unconditionally (move badges(this) outside or
before the ArtistItem check inside subtitleLambda) and also re-add the badges
parameter to the ListItem call (pass badges = badges) so ListItem receives the
badge content as before; key symbols: YouTubeListItem, subtitleLambda, badges,
ListItem, ArtistItem.

In `@test_animatable.kt`:
- Around line 7-16: Replace the ad-hoc main with an automated unit test: remove
or delete the top-level main and instead add a test function (e.g., fun
animatable_bounds_and_animateTo_throws_or_succeeds()) using your test framework
(JUnit or kotlin.test) that sets up Animatable(0.dp, Dp.VectorConverter), calls
updateBounds(0.dp, 100.dp) inside runBlocking, and then asserts the expected
outcome of animateTo(150.dp) (use assertFailsWith<...> if an exception is
expected or assertDoesNotThrow / success assertions otherwise); reference the
existing symbols Animatable, updateBounds, animateTo and runBlocking when
implementing the test so CI will fail if behavior regresses.

In `@test_content_length.kt`:
- Around line 3-5: test_content_length.kt currently contains only a placeholder
main function (fun main) that prints "Hello world"; either implement the
intended content-length test inside this file or remove the file from the PR to
avoid dead test artifacts. If implementing, replace or extend fun main to
include assertions that exercise the content-length logic under test (call the
target functions/classes and assert expected lengths), or convert it into a
proper unit test using your test framework; if not needed, delete
test_content_length.kt so the PR contains no placeholder tests.

---

Outside diff comments:
In `@strip_scaling.py`:
- Around line 3-37: The script never builds or writes out_lines and exits with a
placeholder pass, so implement full transformation: iterate through lines,
append non-skipped lines to out_lines (use the existing flags out_lines,
in_provider, provider_indent, skip_next), detect and drop the whole
CompositionLocalProvider block (start when
"CompositionLocalProvider(LocalDensity provides scaledDensity) {" is seen and
skip until its matching closing brace using a brace counter or stack), skip the
remembered density-related lines (the checks for "val densityScale = remember(",
"val scaledDensity: Density = remember", etc.) as already started, and finally
write the joined out_lines back to the same file; ensure variables and
conditions (out_lines, in_provider, provider_indent, skip_next,
CompositionLocalProvider) are used to locate and remove the intended sections.

---

Nitpick comments:
In `@diff_to_apply.patch`:
- Around line 428-475: You have duplicated logic for extracting artists and the
"otherInfo" string in YouTubeGridItem that mirrors code in YouTubeListItem;
refactor by creating helper functions (e.g., fun extractArtists(item:
YouTubeItem): List<Artist>? and fun extractOtherInfo(item: YouTubeItem):
String?) and replace the inline when blocks in both YouTubeGridItem and
YouTubeListItem to call these helpers; ensure the helpers handle SongItem,
AlbumItem, PlaylistItem, PodcastItem, EpisodeItem cases (matching the existing
logic used for ClickableArtistText and the subtitle Text) so behavior remains
identical.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 85256a13-6ed9-4e6d-91a3-eb808fb7ac74

📥 Commits

Reviewing files that changed from the base of the PR and between b9fec12 and c86d0f8.

⛔ Files ignored due to path filters (1)
  • video.mp4 is excluded by !**/*.mp4
📒 Files selected for processing (11)
  • app/src/main/kotlin/com/metrolist/music/MainActivity.kt
  • app/src/main/kotlin/com/metrolist/music/constants/PreferenceKeys.kt
  • app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt.orig
  • app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt
  • app/src/main/res/values/metrolist_strings.xml
  • diff_to_apply.patch
  • pr_body.md
  • strip_scaling.py
  • test_animatable.kt
  • test_content_length.kt
  • vol.txt
✅ Files skipped from review due to trivial changes (2)
  • vol.txt
  • pr_body.md

Comment thread diff_to_apply.patch Outdated
Comment thread test_animatable.kt Outdated
Comment thread test_content_length.kt Outdated
@kairosci

kairosci commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

@E021ntox I added a toggle for enable it; I've also added validation so that it only works on tablets. If you'd like, give it a try and let me know what you think!

@E021ntox

E021ntox commented Jun 7, 2026

Copy link
Copy Markdown

@E021ntox I added a toggle for enable it; I've also added validation so that it only works on tablets. If you'd like, give it a try and let me know what you think!

It looks like there’s no compiled APK available for testing.

@E021ntox

E021ntox commented Jun 7, 2026

Copy link
Copy Markdown

give it a try and let me know what you think!

#3386 (comment) I think this issue deserves more attention.

@Dr-Brixx

Dr-Brixx commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

@E021ntox I added a toggle for enable it; I've also added validation so that it only works on tablets. If you'd like, give it a try and let me know what you think!

It looks like there’s no compiled APK available for testing.

I think it's here, but I'm not sure

@kairosci

kairosci commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

yes, it is

@E021ntox

E021ntox commented Jun 7, 2026

Copy link
Copy Markdown

yes, it is

It's better now.

@kairosci

kairosci commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

So does that sound okay to you?

@E021ntox

E021ntox commented Jun 7, 2026

Copy link
Copy Markdown

So does that sound okay to you?

Yes.

@E021ntox

E021ntox commented Jun 7, 2026

Copy link
Copy Markdown

Also, I would like to request improving the import feature once again. This is truly crucial for us; otherwise, we won't be able to switch to a new device.

@kairosci

kairosci commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

I still don't quite understand the issue with the import. Could you open a new issue where you explain it in more detail?

@E021ntox

E021ntox commented Jun 7, 2026

Copy link
Copy Markdown

data class CsvImportState(
val previewRows: List<List<String>> = emptyList(),
val artistColumnIndex: Int = 0,
val titleColumnIndex: Int = 1,
val urlColumnIndex: Int = -1,
val hasHeader: Boolean = true,
)

The import function takes urlColumnIndex as a parameter, but this field is never actually used during the import process. Instead, it dynamically matches the video ID based on the title, which results in a high volume of incorrect matches. The proper approach for a clean playlist migration should be importing only the video IDs and then refetching the relevant metadata.

I have already explained the rest of the logic in detail in my previous post.

The playlist import feature is completely broken. I exported my playlist to a CSV file on one phone and imported it onto another. The result is that many songs are incorrect and many others are missing entirely. It appears the import process is not respecting the 'YouTube Video ID' field provided in the CSV.

I believe that when importing metrolist-exported CSVs, the app should rely solely on the 'YouTube Video ID' field. Song titles and artists can be fetched in real-time during the import process. This approach would not only guarantee accuracy but also automatically correct any outdated metadata and match the local language settings.

If a video is deleted or unavailable, the title and artist fields from the CSV can still serve as a useful fallback to show users what song was lost.

Also, please maintain the original order of the CSV file after importing. Currently, the order becomes completely scrambled upon import. I honestly cannot understand why the import feature was designed so defectively.

And the video ID entries were completely ignored and not imported at all. While most of the music IDs imported correctly, there are still a lot of wrong songs.

@E021ntox

E021ntox commented Jun 7, 2026

Copy link
Copy Markdown

The specific steps to reproduce this are very straightforward: just export your playlist to a CSV and import it on another device. You'll notice that the playlist becomes completely mangled, bearing no resemblance to your original one, and the tracks are completely out of order.

@E021ntox

E021ntox commented Jun 7, 2026

Copy link
Copy Markdown

On top of that, there are a few design issues. We're hoarding way too much data in the database, and honestly, a lot of it seems like stuff we don't even need to save.

@kairosci

kairosci commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

a lot of it seems like stuff we don't even need to save.

Like what?

@E021ntox

E021ntox commented Jun 8, 2026

Copy link
Copy Markdown

This button should be removed. I accidentally tapped it and lost my favorite songs without even realizing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Landscape orientation scaling is too zoomed in

3 participants