Hermes Agent commited on
Commit
d4eac6b
·
1 Parent(s): e9fd438

fix(ui): hide topbar on startup/login/mismatch, restore portrait poster card, permanently purge duplicate movies, suppress floating play buttons, and structure landscape metadata

Browse files
apex/repair_jellyfin_libraries.py CHANGED
@@ -200,6 +200,44 @@ def repair_database(db_path: str, marker_file: str | None = None, force_reset_pa
200
  cur.executemany("DELETE FROM BaseItems WHERE Id = ?", [(did,) for did in dup_to_delete])
201
  print(f"[REPAIR] Removed {len(dup_to_delete)} duplicate row(s) pointing to identical paths from {db_path}.")
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  # Check and repair Users table: clear password and login attempt lockouts
204
  cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='Users'")
205
  if cur.fetchone():
 
200
  cur.executemany("DELETE FROM BaseItems WHERE Id = ?", [(did,) for did in dup_to_delete])
201
  print(f"[REPAIR] Removed {len(dup_to_delete)} duplicate row(s) pointing to identical paths from {db_path}.")
202
 
203
+ # Permanent disk + DB deduplication for Movie directories
204
+ movies_dir = "/data/jellyfin/media/Movies"
205
+ if os.path.isdir(movies_dir):
206
+ seen_movie_keys = {}
207
+ for lang in os.listdir(movies_dir):
208
+ lang_path = os.path.join(movies_dir, lang)
209
+ if not os.path.isdir(lang_path):
210
+ continue
211
+ for movie_folder in sorted(os.listdir(lang_path)):
212
+ folder_path = os.path.join(lang_path, movie_folder)
213
+ if not os.path.isdir(folder_path):
214
+ continue
215
+ strm_files = [f for f in os.listdir(folder_path) if f.endswith('.strm')]
216
+ strm_url = None
217
+ if strm_files:
218
+ try:
219
+ with open(os.path.join(folder_path, strm_files[0]), 'r', encoding='utf-8', errors='ignore') as sf:
220
+ strm_url = sf.read().strip()
221
+ except Exception:
222
+ pass
223
+ norm_k = re.sub(r'^(linkz|the)', '', movie_folder, flags=re.I)
224
+ norm_k = re.sub(r'\(\d{4}\)', '', norm_k)
225
+ norm_k = re.sub(r'[^a-zA-Z0-9]', '', norm_k).lower()
226
+
227
+ key = strm_url if strm_url else (norm_k if norm_k else None)
228
+ if key:
229
+ if key in seen_movie_keys:
230
+ existing_folder = seen_movie_keys[key]
231
+ is_curr_worse = 'linkz' in movie_folder.lower() or 'man brand' in movie_folder.lower()
232
+ to_delete = folder_path if is_curr_worse else existing_folder
233
+ to_keep = existing_folder if is_curr_worse else folder_path
234
+ print(f"[REPAIR] Duplicate movie detected: keeping '{to_keep}', deleting duplicate '{to_delete}'")
235
+ shutil.rmtree(to_delete, ignore_errors=True)
236
+ cur.execute("DELETE FROM BaseItems WHERE Path LIKE ?", (to_delete + '%',))
237
+ seen_movie_keys[key] = to_keep
238
+ else:
239
+ seen_movie_keys[key] = folder_path
240
+
241
  # Check and repair Users table: clear password and login attempt lockouts
242
  cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='Users'")
243
  if cur.fetchone():
apex/ui/abyss.css CHANGED
@@ -3281,19 +3281,12 @@ div[role="main"] {
3281
  }
3282
 
3283
 
3284
- /* Suppress floating Play All and Shuffle All in library collection views */
3285
- .page:not(#itemDetailPage):not(#videoOsdPage) .btnPlay,
3286
- .page:not(#itemDetailPage):not(#videoOsdPage) .btnShuffle,
3287
- .viewSettings .btnPlay,
3288
- .viewSettings .btnShuffle,
3289
- .pageTitleWithDefaultButton .btnPlay,
3290
- .pageTitleWithDefaultButton .btnShuffle,
3291
- button[data-action="playall"],
3292
- button[data-action="shuffleall"],
3293
- .mainTabsContainer .btnPlay,
3294
- .mainTabsContainer .btnShuffle,
3295
- .headerRight .btnPlay,
3296
- .headerRight .btnShuffle {
3297
  display: none !important;
3298
  visibility: hidden !important;
3299
  width: 0 !important;
@@ -3301,6 +3294,7 @@ button[data-action="shuffleall"],
3301
  padding: 0 !important;
3302
  margin: 0 !important;
3303
  pointer-events: none !important;
 
3304
  }
3305
 
3306
  /* Hidden duplicate cards */
@@ -4434,14 +4428,26 @@ div[data-category="nextup"] .cardImageContainer img,
4434
  left: 0 !important;
4435
  right: 0 !important;
4436
  width: 100% !important;
4437
- height: 56px !important;
4438
- display: flex !important;
 
 
 
 
4439
  align-items: center !important;
4440
  justify-content: center !important;
4441
- pointer-events: none !important;
4442
  z-index: 99999 !important;
4443
  box-sizing: border-box !important;
4444
  padding: 0 16px !important;
 
 
 
 
 
 
 
 
 
4445
  }
4446
 
4447
  #apex-tv-topbar .apex-tv-tabs {
@@ -4536,22 +4542,34 @@ div[data-category="nextup"] .cardImageContainer img,
4536
  }
4537
  }
4538
 
4539
- /* Never display topbar on splash screen, login screen, server selection, or wizard */
4540
  #loginPage ~ #apex-tv-topbar,
 
 
 
 
 
 
4541
  body:has(#loginPage:not(.hide)) #apex-tv-topbar,
4542
  body:has(.loginPage:not(.hide)) #apex-tv-topbar,
4543
  body:has(#selectServerPage:not(.hide)) #apex-tv-topbar,
 
4544
  body:has(#forgotPasswordPage:not(.hide)) #apex-tv-topbar,
4545
  body:has(.manualLoginForm) #apex-tv-topbar,
 
4546
  body:has(.splashLogo:not(.hide):not(:empty)) #apex-tv-topbar,
4547
- body.is-logged-out #apex-tv-topbar {
 
 
4548
  display: none !important;
4549
  visibility: hidden !important;
4550
  opacity: 0 !important;
4551
  pointer-events: none !important;
4552
  }
4553
 
4554
- /* Clean header on login and splash */
 
 
4555
  #loginPage .skinHeader,
4556
  .loginPage .skinHeader,
4557
  #selectServerPage .skinHeader,
@@ -4605,50 +4623,86 @@ header .pageTitle,
4605
 
4606
  /* ========================================================
4607
  DETAIL PAGE: MOBILE PORTRAIT & DEFAULT CLEAN LAYOUT
4608
- Full-width title, zero text breaking or vertical letter stacking
4609
  ======================================================== */
4610
  @media (orientation: portrait) {
4611
  #itemDetailPage .detailRibbon,
4612
  .layout-mobile #itemDetailPage .detailRibbon {
4613
  display: flex !important;
4614
- flex-direction: column !important;
4615
- align-items: stretch !important;
 
4616
  width: 100% !important;
4617
  max-width: 100% !important;
4618
- padding: 16px 20px !important;
4619
- gap: 12px !important;
4620
  box-sizing: border-box !important;
 
 
4621
  }
4622
 
4623
- #itemDetailPage .infoWrapper,
4624
- .layout-mobile #itemDetailPage .infoWrapper {
4625
- display: flex !important;
4626
- flex-direction: column !important;
4627
- align-items: stretch !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4628
  width: 100% !important;
4629
- max-width: 100% !important;
 
 
 
 
 
 
 
4630
  }
4631
 
4632
- /* On mobile portrait, hide redundant small poster so full-width backdrop and clean title shine */
4633
- .layout-mobile #itemDetailPage .detailImageContainer {
4634
  display: none !important;
4635
- visibility: hidden !important;
4636
- width: 0 !important;
4637
- height: 0 !important;
 
 
 
 
 
 
 
 
4638
  margin: 0 !important;
4639
  padding: 0 !important;
4640
- pointer-events: none !important;
4641
  }
4642
 
4643
  #itemDetailPage .nameContainer,
4644
  .layout-mobile #itemDetailPage .nameContainer {
4645
  display: flex !important;
4646
- flex-direction: row !important;
4647
- align-items: center !important;
4648
- justify-content: space-between !important;
 
4649
  width: 100% !important;
4650
  max-width: 100% !important;
4651
- margin-bottom: 8px !important;
4652
  }
4653
 
4654
  #itemDetailPage .itemName,
@@ -4657,14 +4711,29 @@ header .pageTitle,
4657
  display: block !important;
4658
  width: 100% !important;
4659
  max-width: 100% !important;
4660
- font-size: 1.65rem !important;
4661
  font-weight: 700 !important;
4662
  line-height: 1.25 !important;
4663
  color: #ffffff !important;
4664
  white-space: normal !important;
4665
  word-break: normal !important;
4666
  overflow-wrap: break-word !important;
4667
- letter-spacing: normal !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4668
  }
4669
 
4670
  #itemDetailPage .itemMiscInfo,
@@ -4675,7 +4744,9 @@ header .pageTitle,
4675
  flex-wrap: wrap !important;
4676
  gap: 8px !important;
4677
  width: 100% !important;
4678
- margin-bottom: 12px !important;
 
 
4679
  }
4680
 
4681
  #itemDetailPage .mainDetailButtons,
@@ -4683,9 +4754,13 @@ header .pageTitle,
4683
  display: flex !important;
4684
  flex-direction: row !important;
4685
  align-items: center !important;
4686
- flex-wrap: wrap !important;
4687
  gap: 10px !important;
4688
  width: 100% !important;
 
 
 
 
4689
  }
4690
  }
4691
 
@@ -4858,13 +4933,31 @@ button:has(svg[data-testid="PeopleIcon"]),
4858
  }
4859
 
4860
  #itemDetailPage .nameContainer,
4861
- .layout-mobile #itemDetailPage .nameContainer {
4862
- display: block !important;
 
 
 
 
 
4863
  width: 100% !important;
4864
  max-width: 100% !important;
 
4865
  margin: 0 0 4px 0 !important;
4866
  }
4867
 
 
 
 
 
 
 
 
 
 
 
 
 
4868
  #itemDetailPage .itemName,
4869
  .layout-mobile #itemDetailPage .itemName,
4870
  #itemDetailPage h1.itemName {
 
3281
  }
3282
 
3283
 
3284
+ /* Universal suppression of floating Play All & Shuffle All buttons outside item detail & video */
3285
+ :is(.view, .page, [data-role="page"], .libraryPage):not(#itemDetailPage):not(#videoOsdPage) :is(.btnPlay, .btnShuffle, button[data-action="playall"], button[data-action="shuffleall"], button[title*="Play all" i], button[title*="Shuffle all" i]),
3286
+ .viewSettings :is(.btnPlay, .btnShuffle, button[data-action="play"], button[data-action="shuffle"]),
3287
+ .pageTitleWithDefaultButton :is(.btnPlay, .btnShuffle),
3288
+ .mainTabsContainer :is(.btnPlay, .btnShuffle),
3289
+ .headerRight :is(.btnPlay, .btnShuffle) {
 
 
 
 
 
 
 
3290
  display: none !important;
3291
  visibility: hidden !important;
3292
  width: 0 !important;
 
3294
  padding: 0 !important;
3295
  margin: 0 !important;
3296
  pointer-events: none !important;
3297
+ opacity: 0 !important;
3298
  }
3299
 
3300
  /* Hidden duplicate cards */
 
4428
  left: 0 !important;
4429
  right: 0 !important;
4430
  width: 100% !important;
4431
+ height: 52px !important;
4432
+ display: none !important;
4433
+ visibility: hidden !important;
4434
+ opacity: 0 !important;
4435
+ pointer-events: none !important;
4436
+ flex-direction: row !important;
4437
  align-items: center !important;
4438
  justify-content: center !important;
 
4439
  z-index: 99999 !important;
4440
  box-sizing: border-box !important;
4441
  padding: 0 16px !important;
4442
+ transition: opacity 0.2s ease, visibility 0.2s ease !important;
4443
+ }
4444
+
4445
+ body.show-apex-topbar #apex-tv-topbar,
4446
+ #apex-tv-topbar.is-visible {
4447
+ display: flex !important;
4448
+ visibility: visible !important;
4449
+ opacity: 1 !important;
4450
+ pointer-events: auto !important;
4451
  }
4452
 
4453
  #apex-tv-topbar .apex-tv-tabs {
 
4542
  }
4543
  }
4544
 
4545
+ /* Never display topbar on splash screen, login screen, server selection, server mismatch, or wizard */
4546
  #loginPage ~ #apex-tv-topbar,
4547
+ #serverMismatchPage ~ #apex-tv-topbar,
4548
+ #selectServerPage ~ #apex-tv-topbar,
4549
+ .serverMismatchPage #apex-tv-topbar,
4550
+ #serverMismatchPage #apex-tv-topbar,
4551
+ body:has(#serverMismatchPage) #apex-tv-topbar,
4552
+ body:has(.serverMismatchPage) #apex-tv-topbar,
4553
  body:has(#loginPage:not(.hide)) #apex-tv-topbar,
4554
  body:has(.loginPage:not(.hide)) #apex-tv-topbar,
4555
  body:has(#selectServerPage:not(.hide)) #apex-tv-topbar,
4556
+ body:has(.selectServerPage:not(.hide)) #apex-tv-topbar,
4557
  body:has(#forgotPasswordPage:not(.hide)) #apex-tv-topbar,
4558
  body:has(.manualLoginForm) #apex-tv-topbar,
4559
+ body:has(.manualServerContainer) #apex-tv-topbar,
4560
  body:has(.splashLogo:not(.hide):not(:empty)) #apex-tv-topbar,
4561
+ body.is-logged-out #apex-tv-topbar,
4562
+ body.hide-apex-topbar #apex-tv-topbar,
4563
+ body.videoPlayerContainerVisible #apex-tv-topbar {
4564
  display: none !important;
4565
  visibility: hidden !important;
4566
  opacity: 0 !important;
4567
  pointer-events: none !important;
4568
  }
4569
 
4570
+ /* Clean header on login, mismatch, and splash */
4571
+ #serverMismatchPage .skinHeader,
4572
+ .serverMismatchPage .skinHeader,
4573
  #loginPage .skinHeader,
4574
  .loginPage .skinHeader,
4575
  #selectServerPage .skinHeader,
 
4623
 
4624
  /* ========================================================
4625
  DETAIL PAGE: MOBILE PORTRAIT & DEFAULT CLEAN LAYOUT
4626
+ Clean poster card thumbnail + left-aligned stacked title & metadata
4627
  ======================================================== */
4628
  @media (orientation: portrait) {
4629
  #itemDetailPage .detailRibbon,
4630
  .layout-mobile #itemDetailPage .detailRibbon {
4631
  display: flex !important;
4632
+ flex-direction: row !important;
4633
+ align-items: flex-start !important;
4634
+ flex-wrap: wrap !important;
4635
  width: 100% !important;
4636
  max-width: 100% !important;
4637
+ padding: 16px 16px 12px 16px !important;
4638
+ gap: 14px !important;
4639
  box-sizing: border-box !important;
4640
+ position: relative !important;
4641
+ background: transparent !important;
4642
  }
4643
 
4644
+ /* Fixed Clean Poster Thumbnail in Portrait */
4645
+ #itemDetailPage .detailImageContainer,
4646
+ .layout-mobile #itemDetailPage .detailImageContainer {
4647
+ display: block !important;
4648
+ visibility: visible !important;
4649
+ opacity: 1 !important;
4650
+ position: relative !important;
4651
+ width: 108px !important;
4652
+ min-width: 108px !important;
4653
+ max-width: 108px !important;
4654
+ height: 162px !important;
4655
+ aspect-ratio: 2 / 3 !important;
4656
+ border-radius: 10px !important;
4657
+ overflow: hidden !important;
4658
+ margin: 0 !important;
4659
+ padding: 0 !important;
4660
+ flex-shrink: 0 !important;
4661
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.75) !important;
4662
+ border: 1px solid rgba(255, 255, 255, 0.14) !important;
4663
+ }
4664
+
4665
+ #itemDetailPage .detailImageContainer .card,
4666
+ .layout-mobile #itemDetailPage .detailImageContainer .card,
4667
+ #itemDetailPage .detailImageContainer .cardScalable {
4668
  width: 100% !important;
4669
+ height: 100% !important;
4670
+ aspect-ratio: 2 / 3 !important;
4671
+ border-radius: 10px !important;
4672
+ overflow: hidden !important;
4673
+ margin: 0 !important;
4674
+ position: relative !important;
4675
+ top: 0 !important;
4676
+ left: 0 !important;
4677
  }
4678
 
4679
+ #itemDetailPage .detailImageContainer .cardPadder {
 
4680
  display: none !important;
4681
+ }
4682
+
4683
+ #itemDetailPage .infoWrapper,
4684
+ .layout-mobile #itemDetailPage .infoWrapper {
4685
+ display: flex !important;
4686
+ flex-direction: column !important;
4687
+ align-items: flex-start !important;
4688
+ justify-content: flex-start !important;
4689
+ flex: 1 1 calc(100% - 128px) !important;
4690
+ min-width: 0 !important;
4691
+ gap: 3px !important;
4692
  margin: 0 !important;
4693
  padding: 0 !important;
 
4694
  }
4695
 
4696
  #itemDetailPage .nameContainer,
4697
  .layout-mobile #itemDetailPage .nameContainer {
4698
  display: flex !important;
4699
+ flex-direction: column !important;
4700
+ align-items: flex-start !important;
4701
+ justify-content: flex-start !important;
4702
+ gap: 2px !important;
4703
  width: 100% !important;
4704
  max-width: 100% !important;
4705
+ margin: 0 0 4px 0 !important;
4706
  }
4707
 
4708
  #itemDetailPage .itemName,
 
4711
  display: block !important;
4712
  width: 100% !important;
4713
  max-width: 100% !important;
4714
+ font-size: 1.45rem !important;
4715
  font-weight: 700 !important;
4716
  line-height: 1.25 !important;
4717
  color: #ffffff !important;
4718
  white-space: normal !important;
4719
  word-break: normal !important;
4720
  overflow-wrap: break-word !important;
4721
+ letter-spacing: -0.01em !important;
4722
+ margin: 0 !important;
4723
+ padding: 0 !important;
4724
+ text-align: left !important;
4725
+ }
4726
+
4727
+ #itemDetailPage .originalTitle,
4728
+ #itemDetailPage .parentName,
4729
+ #itemDetailPage .itemName.originalTitle,
4730
+ #itemDetailPage .nameContainer > *:not(h1) {
4731
+ font-size: 0.95rem !important;
4732
+ font-weight: 500 !important;
4733
+ color: rgba(255, 255, 255, 0.72) !important;
4734
+ margin: 0 !important;
4735
+ padding: 0 !important;
4736
+ text-align: left !important;
4737
  }
4738
 
4739
  #itemDetailPage .itemMiscInfo,
 
4744
  flex-wrap: wrap !important;
4745
  gap: 8px !important;
4746
  width: 100% !important;
4747
+ margin: 2px 0 6px 0 !important;
4748
+ color: rgba(255, 255, 255, 0.72) !important;
4749
+ font-size: 0.85rem !important;
4750
  }
4751
 
4752
  #itemDetailPage .mainDetailButtons,
 
4754
  display: flex !important;
4755
  flex-direction: row !important;
4756
  align-items: center !important;
4757
+ flex-wrap: nowrap !important;
4758
  gap: 10px !important;
4759
  width: 100% !important;
4760
+ flex-basis: 100% !important;
4761
+ margin-top: 10px !important;
4762
+ overflow-x: auto !important;
4763
+ padding-bottom: 4px !important;
4764
  }
4765
  }
4766
 
 
4933
  }
4934
 
4935
  #itemDetailPage .nameContainer,
4936
+ .layout-mobile #itemDetailPage .nameContainer,
4937
+ .layout-desktop #itemDetailPage .nameContainer,
4938
+ .layout-tv #itemDetailPage .nameContainer {
4939
+ display: flex !important;
4940
+ flex-direction: column !important;
4941
+ align-items: flex-start !important;
4942
+ justify-content: flex-start !important;
4943
  width: 100% !important;
4944
  max-width: 100% !important;
4945
+ gap: 2px !important;
4946
  margin: 0 0 4px 0 !important;
4947
  }
4948
 
4949
+ #itemDetailPage .originalTitle,
4950
+ #itemDetailPage .parentName,
4951
+ #itemDetailPage .itemName.originalTitle,
4952
+ #itemDetailPage .nameContainer > *:not(h1) {
4953
+ font-size: 1.1rem !important;
4954
+ font-weight: 500 !important;
4955
+ color: rgba(255, 255, 255, 0.75) !important;
4956
+ margin: 2px 0 0 0 !important;
4957
+ padding: 0 !important;
4958
+ text-align: left !important;
4959
+ }
4960
+
4961
  #itemDetailPage .itemName,
4962
  .layout-mobile #itemDetailPage .itemName,
4963
  #itemDetailPage h1.itemName {
apex/ui/spotlight-loader.js CHANGED
@@ -764,31 +764,49 @@ function findHomeTab() {
764
 
765
  if (tvTopbar) {
766
  var isVideo = !!document.querySelector(".videoPlayerContainer:not(.hide), #videoOsdPage:not(.hide), video");
767
- var hash = window.location.hash || "";
 
 
 
 
 
768
  var isHiddenRoute = hash.indexOf("dashboard") !== -1 ||
769
  hash.indexOf("settings") !== -1 ||
770
  hash.indexOf("login") !== -1 ||
771
  hash.indexOf("selectserver") !== -1 ||
 
772
  hash.indexOf("forgotpassword") !== -1 ||
773
  hash.indexOf("details") !== -1 ||
774
  hash.indexOf("itemdetails") !== -1;
775
 
776
  var isDetailPage = !!document.querySelector("#itemDetailPage:not(.hide), .itemDetailPage:not(.hide)");
777
- var isAuthPage = !!document.querySelector("#loginPage:not(.hide), .loginPage:not(.hide), #selectServerPage:not(.hide), #forgotPasswordPage:not(.hide), .manualLoginForm, form[data-action='login'], input[type='password']");
778
- var isSplash = !!document.querySelector(".splashLogo:not(.hide), #splashPage:not(.hide), .splashLogo");
779
- var hasHomeOrLibrary = !!document.querySelector("#indexPage, .homePage, #homeTab, .pageTabContent, .mainDrawer, .itemsContainer");
780
  var isUserLoggedIn = (window.ApiClient && typeof window.ApiClient.isLoggedIn === "function") ? window.ApiClient.isLoggedIn() : (window.ApiClient && !!window.ApiClient.accessToken());
781
 
782
- if (isVideo || isHiddenRoute || isDetailPage || isAuthPage || (isSplash && !hasHomeOrLibrary) || (!isUserLoggedIn && !hasHomeOrLibrary)) {
 
 
783
  tvTopbar.style.display = "none";
784
  tvTopbar.style.visibility = "hidden";
785
  return;
786
  } else {
 
 
787
  tvTopbar.style.display = "flex";
788
  tvTopbar.style.visibility = "visible";
789
  }
790
  }
791
 
 
 
 
 
 
 
 
 
 
 
 
792
  // 2. Hide Hamburger, Cast, SyncPlay, and stray drawer buttons completely
793
  var unwanted = document.querySelectorAll(
794
  'button[aria-label="Menu"], button[aria-label="menu"], ' +
@@ -901,31 +919,30 @@ function findHomeTab() {
901
 
902
  function deduplicateDomCards() {
903
  safe(function () {
904
- var containers = document.querySelectorAll(".itemsContainer");
905
- for (var i = 0; i < containers.length; i++) {
906
- var container = containers[i];
907
- var cards = container.querySelectorAll(":scope > .card, :scope > div > .card");
908
- if (!cards || !cards.length) cards = container.children;
909
- var seenTitles = {};
910
- for (var j = 0; j < cards.length; j++) {
911
- var card = cards[j];
912
- if (!card.classList || !card.classList.contains("card")) continue;
913
- var titleEl = card.querySelector(".cardText-first") || card.querySelector(".cardText:not(.cardText-secondary)") || card.querySelector(".cardTextCentered") || card.querySelector(".cardText");
914
- var rawTitle = (titleEl ? titleEl.textContent : (card.getAttribute("aria-label") || "")).trim().toLowerCase();
915
- var normTitle = rawTitle.replace(/\s*\(\s*\d{4}\s*\)\s*/g, "").replace(/[^a-z0-9]/g, "");
916
- if (!normTitle) continue;
917
- if (seenTitles[normTitle]) {
918
- card.classList.add("apex-duplicate-hidden");
919
- card.style.setProperty("display", "none", "important");
920
- card.style.setProperty("visibility", "hidden", "important");
921
- card.style.setProperty("width", "0px", "important");
922
- card.style.setProperty("height", "0px", "important");
923
- card.style.setProperty("flex", "0 0 0px", "important");
924
- card.style.setProperty("margin", "0px", "important");
925
- card.style.setProperty("padding", "0px", "important");
926
- } else {
927
- seenTitles[normTitle] = true;
928
- }
929
  }
930
  }
931
  });
 
764
 
765
  if (tvTopbar) {
766
  var isVideo = !!document.querySelector(".videoPlayerContainer:not(.hide), #videoOsdPage:not(.hide), video");
767
+ var hash = (window.location.hash || "").toLowerCase();
768
+ var isAuthOrStartup = !!document.querySelector(
769
+ "#serverMismatchPage, .serverMismatchPage, #loginPage:not(.hide), .loginPage:not(.hide), " +
770
+ "#selectServerPage:not(.hide), .selectServerPage:not(.hide), #forgotPasswordPage:not(.hide), " +
771
+ ".manualLoginForm, .splashLogo:not(.hide), .manualServerContainer, input[type='password']"
772
+ );
773
  var isHiddenRoute = hash.indexOf("dashboard") !== -1 ||
774
  hash.indexOf("settings") !== -1 ||
775
  hash.indexOf("login") !== -1 ||
776
  hash.indexOf("selectserver") !== -1 ||
777
+ hash.indexOf("servermismatch") !== -1 ||
778
  hash.indexOf("forgotpassword") !== -1 ||
779
  hash.indexOf("details") !== -1 ||
780
  hash.indexOf("itemdetails") !== -1;
781
 
782
  var isDetailPage = !!document.querySelector("#itemDetailPage:not(.hide), .itemDetailPage:not(.hide)");
 
 
 
783
  var isUserLoggedIn = (window.ApiClient && typeof window.ApiClient.isLoggedIn === "function") ? window.ApiClient.isLoggedIn() : (window.ApiClient && !!window.ApiClient.accessToken());
784
 
785
+ if (isVideo || isHiddenRoute || isDetailPage || isAuthOrStartup || !isUserLoggedIn) {
786
+ tvTopbar.classList.remove("is-visible");
787
+ document.body.classList.remove("show-apex-topbar");
788
  tvTopbar.style.display = "none";
789
  tvTopbar.style.visibility = "hidden";
790
  return;
791
  } else {
792
+ tvTopbar.classList.add("is-visible");
793
+ document.body.classList.add("show-apex-topbar");
794
  tvTopbar.style.display = "flex";
795
  tvTopbar.style.visibility = "visible";
796
  }
797
  }
798
 
799
+ // 1.5 Suppress and purge floating Play / Shuffle buttons from catalog/library views
800
+ var strayPlayBtns = document.querySelectorAll(
801
+ ':is(.view, .page, [data-role="page"], .libraryPage):not(#itemDetailPage):not(#videoOsdPage) :is(.btnPlay, .btnShuffle, button[data-action="playall"], button[data-action="shuffleall"], button[title*="Play all" i], button[title*="Shuffle all" i]), ' +
802
+ '.viewSettings :is(.btnPlay, .btnShuffle, button[data-action="play"], button[data-action="shuffle"]), ' +
803
+ '.pageTitleWithDefaultButton :is(.btnPlay, .btnShuffle), ' +
804
+ '.mainTabsContainer :is(.btnPlay, .btnShuffle)'
805
+ );
806
+ for (var sp = 0; sp < strayPlayBtns.length; sp++) {
807
+ strayPlayBtns[sp].remove();
808
+ }
809
+
810
  // 2. Hide Hamburger, Cast, SyncPlay, and stray drawer buttons completely
811
  var unwanted = document.querySelectorAll(
812
  'button[aria-label="Menu"], button[aria-label="menu"], ' +
 
919
 
920
  function deduplicateDomCards() {
921
  safe(function () {
922
+ var activePage = document.querySelector('.page:not(.hide), .view:not(.hide), [data-role="page"]:not(.hide)') || document;
923
+ var cards = activePage.querySelectorAll('.card');
924
+ if (!cards || !cards.length) return;
925
+ var seenTitles = {};
926
+ for (var j = 0; j < cards.length; j++) {
927
+ var card = cards[j];
928
+ if (!card.classList || !card.classList.contains("card")) continue;
929
+ if (card.closest('#itemDetailPage') || card.closest('.detailImageContainer')) continue;
930
+ var titleEl = card.querySelector(".cardText-first") || card.querySelector(".cardText:not(.cardText-secondary)") || card.querySelector(".cardTextCentered") || card.querySelector(".cardText");
931
+ var rawTitle = (titleEl ? titleEl.textContent : (card.getAttribute("aria-label") || "")).trim().toLowerCase();
932
+ var normTitle = rawTitle.replace(/\s*\(\s*\d{4}\s*\)\s*/g, "").replace(/^(linkz|the)\b/g, "").replace(/[^a-z0-9]/g, "");
933
+ if (!normTitle) continue;
934
+ if (seenTitles[normTitle]) {
935
+ card.classList.add("apex-duplicate-hidden");
936
+ card.style.setProperty("display", "none", "important");
937
+ card.style.setProperty("visibility", "hidden", "important");
938
+ card.style.setProperty("width", "0px", "important");
939
+ card.style.setProperty("height", "0px", "important");
940
+ card.style.setProperty("flex", "0 0 0px", "important");
941
+ card.style.setProperty("margin", "0px", "important");
942
+ card.style.setProperty("padding", "0px", "important");
943
+ card.remove();
944
+ } else {
945
+ seenTitles[normTitle] = true;
 
946
  }
947
  }
948
  });