Skip to content

RichHome layout, artwork stability, Settings Permissions/Now Playing source - #29

Merged
tukuyomil032 merged 44 commits into
mainfrom
fix/richhome-layout-and-artwork
Jul 31, 2026
Merged

RichHome layout, artwork stability, Settings Permissions/Now Playing source#29
tukuyomil032 merged 44 commits into
mainfrom
fix/richhome-layout-and-artwork

Conversation

@tukuyomil032

@tukuyomil032 tukuyomil032 commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

Phase B7〜B13の実機フィードバック対応をまとめたPR。

  • RichHome(展開UI)のレイアウト微調整(歌詞の垂直/水平センタリング、4行ウィンドウ表示化、
    シークバー位置、trackInfo位置、ヘッダー整理)
  • 命名整理: AtollStyleExpandedViewRichHomeView(参考先アプリ名の除去、機械的リネーム)
  • アートワーク取得・再生コントロールの安定化
    • Spotifyアートワークキャッシュの成功後書き込み化(一度失敗すると永久に再試行されないバグ)
    • Apple Musicアートワークのバウンド付き再試行
    • NSAppleEventsUsageDescription欠落によりAutomation許可プロンプトが一切出ない不具合を修正
    • CFBundleIdentifier欠落による起動方法別の権限不整合を修正
    • AppleScript失敗の診断ログ追加
  • 設定画面: 「権限」タブ新設(Calendar/Screen Recording/Automation状態確認・付与)、
    Now Playingソース選択を独立トグル3つから排他Picker(自動/Spotify/Apple Music/YouTube Music)
    に置き換え、設定ウィンドウ拡幅
  • バグ修正: 起動時に既に再生中の音楽が検知されない問題(能動的な初回状態取得を追加)

Test plan

  • 各コミットで xcodebuild build 成功を確認済み
  • xcodebuild test -only-testing:perchTests 全体を複数回実行、回帰なしを確認済み
  • PreferencesMigrationTests に新規移行ロジックのテストを追加
  • 実機での通し検証(Automation権限プロンプトの表示、ソース切替の即時反映、起動時検知)

🤖 Generated with Claude Code

https://claude.ai/code/session_01EFKkNRhr4QbyBdZKNtPm8d

Summary by CodeRabbit

  • 新機能

    • Now Playingで優先する音楽ソースを選択できるようになりました。
    • 権限の状態確認、許可要求、システム設定への移動に対応しました。
    • 起動時や設定変更時に再生情報を自動検出します。
    • 歌詞表示を見やすい固定4行表示に改善しました。
  • 改善

    • アートワーク取得の再試行とエラー処理を強化しました。
    • Now Playingカードのレイアウトとソース表示を調整しました。
    • 既存の音楽ソース設定を新しい選択方式へ自動移行します。

tukuyomil032 and others added 30 commits July 31, 2026 12:45
… fixes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renames the type/file and rewords the handful of comments that named a
specific reference app, replacing them with neutral descriptions of the
actual design intent (rounder pill chrome, fuller rich-mode layout).
Purely mechanical — no behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…w-playing card

The row's alignment was .top, but centerColumn is height-locked to
SurfaceMetrics.lyricsColumnHeight (130pt) while NowPlayingCard runs
220-260pt — top-aligning pinned the lyrics box to the row's top edge
with dead space below it. Changed to .center.

Also gives the HStack an explicit idealWidth. The vendored NookView
lays out the expanded surface under an unconstrained (nil) size
proposal via .fixedSize(), so without an idealWidth the HStack's own
ideal width was just NowPlayingCard's ideal plus the lyrics text's
natural width — narrower than the 640pt window floor, so the outer
frame(minWidth:) centered the whole narrow HStack as a block instead
of letting centerColumn's maxWidth:.infinity actually fill the window.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
progressSection sat right below the artwork/trackInfo row with only the
outer VStack's 10pt spacing between them, reading as too tight. Added
8pt of top padding on progressSection itself, leaving its own internal
spacing and the gap to controlsSection unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The black circle chip behind the source logo/symbol read as an
unexplained decoration. Removed it and enlarged the icon from 18pt to
40pt (roughly a third of the 128pt artwork) so it reads clearly on its
own, keeping the existing 0.3s delayed bounce-in.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… download failure

lastSpotifyURL was written before the download attempt, so a single
transient failure (network flakiness, DNS) permanently blocked retries
for that track — the same URL would never pass the dedup guard again.
Moved the write to after a successful download; NowPlayingManager's
existing needsFetch check (state.artwork == nil) already retries on
the next notification when it's missing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
com.apple.Music.playerInfo only fires on Play/Pause toggle or track
change (no frequent position notifications like Spotify), so a single
failed AppleScript artwork fetch right after a track change had no
further chance to retry. Piggybacks on the existing 1.5s
pollAppleMusicPosition() loop instead of adding a bespoke timer:
retries up to 3 times (~4.5s window) per track when artwork is still
nil, then gives up so a track that genuinely has none doesn't retry
forever.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cleanup

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The old ScrollView + gradient mask never actually hid off-window
lines — lineOpacity() floored at 0.20-0.65 rather than 0, and the
mask only faded the outer 5-8% of the view, so lines beyond the
intended 3-4 always showed through faintly. Replaced with a plain
VStack rendering exactly the 4 lines in view (fewer if the song has
less), computed from activeIndex and crossfaded when the window
shifts — no lines exist outside that set, so there's nothing left to
peek through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…the row

Centering centerColumn against NowPlayingCard's own height ignores that
IslandTopBar (44pt) plus its Divider sit above the whole row — the
header's height visibly pulls the true center of the visible pill
higher than NowPlayingCard's center alone, so lyrics rendered noticeably
low relative to the full panel. Added SurfaceMetrics.headerHeight (an
approximation derived from the header's own padding/control sizes) and
offset centerColumn up by half of it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous fix (.padding(.top, 8)) only widened the gap above
progressSection, leaving it 18pt from the artwork row but only 10pt
from controlsSection — still visibly closer to the controls than
centered. Changed to .padding(.vertical, 8) so both gaps match at
18pt, putting the seek bar block midway between the two.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dropped trackInfo's top Spacer so title/artist/album pin to the
artwork's top edge instead of self-centering in the 128pt row — the
enlarged source badge (Phase B7) sits closer to a vertically centered
block than a top-aligned one. Also removed the album Text's
lineLimit(2) so long album names wrap in full instead of truncating.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Moving the cursor off the expanded surface already collapses it (the
existing hover-exit behavior), so the explicit xmark button was
redundant chrome. AppState.collapse() itself is untouched — kept as a
public API other triggers may use, and still covered by AppStateTests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ModuleSwitcher's HStack spacing (24pt) plus each button's own 6pt
horizontal padding read as an unnaturally wide gap between the two
icons. Reduced spacing to 8pt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eaks

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The custom Info.plist never referenced PRODUCT_BUNDLE_IDENTIFIER, so
every build shipped with no CFBundleIdentifier at all — codesign
synthesized a different fallback identifier per build environment
(confirmed: `just run`'s DerivedData build signed as "perch" with a
Development cert/Team ID, while /Applications/perch.app carried an
ad-hoc-signed hashed identifier with no team). macOS's Automation TCC
grants are scoped per signing identity, so the two were tracked as
separate apps — Automation permission granted to one (enabling
artwork fetch and playback controls, both AppleScript-driven) simply
didn't apply to the other. Verified the built app now reports
Identifier=com.tukuyomi032.perch via codesign.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase B8 pinned title/artist/album flush to the artwork's top edge to
put distance between them and the enlarged source badge — too far up
per follow-up feedback. Added 10pt of top padding as a middle ground,
still clearly separated from the badge but not pinned to the very top.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Active-line emphasis (spring response 0.40) and window crossfade
(easeInOut 0.4s) both read as too abrupt per feedback. Bumped to
0.55 for each — timestamp-driven timing (activeIndex) is unchanged,
only how the transition itself moves.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ostics

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Without a usage description string, macOS never shows the Automation
permission prompt for AppleScript control of Spotify/Music — it just
fails silently (errAEEventNotPermitted, -1743) on the first attempt.
This combined with the Phase B9 CFBundleIdentifier fix (which makes
the app a genuinely new TCC identity requiring fresh consent) left
artwork fetching and playback controls broken on every launch path,
since there was no way to ever get prompted for permission. Verified
the string now appears in the built app's Info.plist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously swallowed errors silently, making it impossible to tell a
permission denial (-1743) from any other failure when playback
controls or artwork stopped working. Logs the NSAppleScript error
number and message at debug level.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Added a Logger (matching NowPlayingManager's pattern) and logging on
both AppleScript failure paths (fetchAppleMusicArtworkData, the
shared runAppleScript used by Spotify's URL fetch), distinguishing
permission denial (-1743) from other AppleScript errors and from a
track that simply has no artwork.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switched the VStack, multilineTextAlignment, and each line's frame
alignment from center to leading per feedback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tukuyomil032 and others added 14 commits July 31, 2026 14:48
…tatus

New @observable store following CalendarStore's authorization-state
pattern. refreshAll() preflight-checks all four permissions without
ever prompting (AEDeterminePermissionToAutomateTarget with
askUserIfNeeded: false for Automation, CGPreflightScreenCaptureAccess
for Screen Recording, EKEventStore.authorizationStatus for Calendar).
request(_:) triggers the real OS prompt when a permission is still
notDetermined; once denied, macOS won't show another system dialog no
matter how many times the app asks, so request(_:) opens the relevant
System Settings pane instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lists Calendar, Screen Recording, and Automation (Spotify/Apple
Music) with a live status (color-coded) and a per-row action button —
"request" when not yet determined, "open System Settings" once
denied (macOS won't show another system prompt for a denied
permission, so that's the only remaining path). Status refreshes via
PermissionsStore.refreshAll() on .onAppear.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
en/ja pairs for the tab label, section header, permission names,
status labels, and action buttons. Verified both bundle correctly
into the built app's .lproj resources.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
420pt was sized for 6 tabs; adding Permissions in Phase B11 pushed it
to 7, which no longer fit and pushed Permissions/Updates into
TabView's ">>" overflow menu — unreachable per user report. Widened
to 600pt so all tabs render in a single row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User reconsidered the Phase B10 left-alignment change and asked to
go back to centered.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bumped LyricsView's default fontSize and NowPlayingLyricsColumn's
explicit call site to match. Updated lyricsColumnHeight's derivation
comment for the new font size — the 130pt value itself is unchanged,
still a floor with a little slack over the ~118pt estimate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tup detection

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ger font size

Phase B12's font bump (13pt -> 15pt) pushed some lyric lines from 2
wrapped lines to 3, hitting lineLimit(2) and truncating with an
ellipsis instead of wrapping fully. scaleEffect on the active line
doesn't affect layout/wrapping (it's a post-layout render transform),
so this affected any line long enough to need a third line, not just
the active one.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e picker

Two bugs fixed:

1. The three Spotify/Apple Music/YouTube Music toggles were checked
   only when a new notification arrived, with no way to clear an
   already-active state or react to the toggle itself changing —
   turning a source off while it was playing left its activity
   showing indefinitely. Replaced with a single
   NowPlayingSourcePreference (auto/spotify/appleMusic/youTubeMusic),
   gated once in applyState, and observed via Defaults.observe so a
   change clears currentState immediately when it no longer matches.

2. A track already playing when Perch launches was never detected —
   Spotify/Music's DistributedNotifications only fire on a state
   *transition*, so nothing arrived until the user paused/resumed.
   Added active AppleScript queries (detectActiveSpotifyState,
   detectActiveAppleMusicState) plus a MediaRemoteBridge.currentState
   read for YouTube Music, run once at launch and again whenever the
   preferred source changes.

Settings' NowPlaying tab now shows one menu-style Picker instead of
three toggles, matching the single-select pattern requested.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follows migrateIslandChromeStyle's shape: gated on the legacy keys
actually having been written (object(forKey:) != nil), consuming
them once migrated. Exactly one enabled source carries over as that
explicit choice; the old all-enabled default and any other
combination fall back to .auto. Added PreferencesMigrationTests
coverage for the pure mapping function and the store-level migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tukuyomil032 tukuyomil032 self-assigned this Jul 31, 2026
@tukuyomil032
tukuyomil032 merged commit 61f4654 into main Jul 31, 2026
3 of 4 checks passed
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1262a7e5-8bfe-4faa-9ae5-5372ad80ec12

📥 Commits

Reviewing files that changed from the base of the PR and between ef968a2 and e190f0c.

📒 Files selected for processing (26)
  • docs/progress.md
  • perch/Core/NowPlayingSourcePreference.swift
  • perch/Core/Preferences.swift
  • perch/Core/PreferencesMigration.swift
  • perch/Core/UIMode.swift
  • perch/Features/Calendar/CalendarStandaloneView.swift
  • perch/Features/Calendar/TodayEventsColumn.swift
  • perch/Features/NowPlaying/ArtworkFetcher.swift
  • perch/Features/NowPlaying/LyricsView.swift
  • perch/Features/NowPlaying/NowPlayingCard.swift
  • perch/Features/NowPlaying/NowPlayingLyricsColumn.swift
  • perch/Features/NowPlaying/NowPlayingManager.swift
  • perch/Features/Permissions/PermissionsStore.swift
  • perch/Island/IslandChromeStyle.swift
  • perch/Island/IslandHost.swift
  • perch/Resources/Info.plist
  • perch/Resources/en.lproj/Localizable.strings
  • perch/Resources/ja.lproj/Localizable.strings
  • perch/UI/ExpandedIslandView.swift
  • perch/UI/IslandTopBar.swift
  • perch/UI/RichHomeView.swift
  • perch/UI/SettingsView.swift
  • perch/UI/SurfaceMetrics.swift
  • perch/UI/SurfaceSizeResolver.swift
  • perch/Vendor/NookSurface/NookStyle.swift
  • perchTests/Core/PreferencesMigrationTests.swift

📝 Walkthrough

Walkthrough

Now Playing のソース選択と旧設定移行を追加した。起動時検出、権限管理、アートワーク取得、歌詞表示、Rich UI のレイアウトと名称を更新した。

Changes

Now Playing ソース設定と移行

Layer / File(s) Summary
ソース設定、移行、テスト
perch/Core/..., perchTests/Core/..., docs/progress.md
NowPlayingSourcePreference と新しい設定キーを追加した。旧3トグルからの移行、旧キー削除、既存設定の維持を実装してテストした。

Now Playing 検出と権限

Layer / File(s) Summary
再生ソース検出とアートワーク取得
perch/Features/NowPlaying/...
起動時と設定変更時に再生状態を再検出する。AppleScript のエラー記録と Apple Music のアートワーク再試行を追加した。
権限管理と設定画面
perch/Features/Permissions/..., perch/UI/SettingsView.swift, perch/Resources/..., perch/Resources/Info.plist
権限状態の確認と要求を追加した。設定画面に権限タブと Now Playing ソース選択を追加した。

Rich UI と Now Playing 表示

Layer / File(s) Summary
Rich UI、歌詞、再生情報レイアウト
perch/UI/..., perch/Features/NowPlaying/..., perch/Features/Calendar/..., perch/Island/..., perch/Vendor/..., docs/progress.md
RichHomeView への名称変更、モジュール切替、固定4行の歌詞表示、再生元バッジ、曲情報、進行表示のレイアウトを更新した。関連コメントと進捗記録も更新した。

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SettingsView
  participant NowPlayingManager
  participant Spotify
  participant AppleMusic
  participant YouTubeMusic
  SettingsView->>NowPlayingManager: preferredNowPlayingSource の変更を通知する
  NowPlayingManager->>Spotify: 再生状態を取得する
  NowPlayingManager->>AppleMusic: 再生状態を取得する
  NowPlayingManager->>YouTubeMusic: MediaRemote 状態を取得する
  NowPlayingManager->>SettingsView: 選択ソースの状態を表示する
Loading

Possibly related PRs

Poem

うさぎは再生元を選び
歌詞の窓を四行にそろえ
権限の道をひらきます
Rich Home に月が照り
音楽は静かに跳ねました crescent

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/richhome-layout-and-artwork

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.

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.

1 participant