iOS: add chapters list sheet to full player
Some checks failed
CI / UI / Build (pull_request) Failing after 6s
CI / Scraper / Test (pull_request) Failing after 6s
CI / Scraper / Lint (pull_request) Failing after 8s
CI / Scraper / Build (pull_request) Has been skipped
iOS CI / Build (push) Has been cancelled
iOS CI / Test (push) Has been cancelled
iOS CI / Build (pull_request) Failing after 2s
iOS CI / Test (pull_request) Has been skipped
Some checks failed
CI / UI / Build (pull_request) Failing after 6s
CI / Scraper / Test (pull_request) Failing after 6s
CI / Scraper / Lint (pull_request) Failing after 8s
CI / Scraper / Build (pull_request) Has been skipped
iOS CI / Build (push) Has been cancelled
iOS CI / Test (push) Has been cancelled
iOS CI / Build (pull_request) Failing after 2s
iOS CI / Test (pull_request) Has been skipped
- List bullet button now opens a chapters list sheet - ChaptersListSheet displays all chapters with scroll view - Current chapter highlighted with amber badge and background - Auto-scrolls to current chapter when opened - Chapter selection uses skip notifications to navigate - Supports both .medium and .large presentation sizes - Shows "Now Playing" label on current chapter - Clean list design with chapter numbers and titles
This commit is contained in:
@@ -190,6 +190,7 @@ struct FullPlayerView: View {
|
||||
@State private var isFavorited = false
|
||||
@State private var isLiked = false
|
||||
@State private var showingSpeedMenu = false
|
||||
@State private var showingChaptersList = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -515,14 +516,15 @@ struct FullPlayerView: View {
|
||||
.disabled(true)
|
||||
|
||||
// Queue (show chapters list)
|
||||
Button {} label: {
|
||||
Button {
|
||||
showingChaptersList = true
|
||||
} label: {
|
||||
Image(systemName: "list.bullet")
|
||||
.font(.system(size: 22))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(true)
|
||||
|
||||
// Sleep timer (placeholder)
|
||||
Button {} label: {
|
||||
@@ -538,6 +540,31 @@ struct FullPlayerView: View {
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingChaptersList) {
|
||||
ChaptersListSheet(
|
||||
chapters: audioPlayer.chapters,
|
||||
currentChapter: audioPlayer.chapter,
|
||||
onChapterSelect: { selectedChapter in
|
||||
showingChaptersList = false
|
||||
// Post notification to navigate to selected chapter
|
||||
if selectedChapter > audioPlayer.chapter {
|
||||
NotificationCenter.default.post(
|
||||
name: .skipToNextChapter,
|
||||
object: nil,
|
||||
userInfo: ["next": selectedChapter]
|
||||
)
|
||||
} else if selectedChapter < audioPlayer.chapter {
|
||||
NotificationCenter.default.post(
|
||||
name: .skipToPrevChapter,
|
||||
object: nil,
|
||||
userInfo: ["prev": selectedChapter]
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
|
||||
private func formatTime(_ seconds: Double) -> String {
|
||||
@@ -575,6 +602,92 @@ struct FullPlayerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Chapters List Sheet
|
||||
|
||||
struct ChaptersListSheet: View {
|
||||
let chapters: [ChapterIndexBrief]
|
||||
let currentChapter: Int
|
||||
let onChapterSelect: (Int) -> Void
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollViewReader { proxy in
|
||||
List {
|
||||
ForEach(chapters, id: \.number) { chapter in
|
||||
Button {
|
||||
onChapterSelect(chapter.number)
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
// Chapter number badge
|
||||
Text("\(chapter.number)")
|
||||
.font(.caption.bold())
|
||||
.foregroundStyle(chapter.number == currentChapter ? .white : .secondary)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(chapter.number == currentChapter ? Color.amber : Color.gray.opacity(0.2))
|
||||
)
|
||||
|
||||
// Chapter title
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(chapter.title.strippingTrailingDate())
|
||||
.font(.subheadline.weight(chapter.number == currentChapter ? .semibold : .regular))
|
||||
.foregroundStyle(chapter.number == currentChapter ? .primary : .primary)
|
||||
.lineLimit(2)
|
||||
|
||||
if chapter.number == currentChapter {
|
||||
Text("Now Playing")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.amber)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Checkmark for current chapter
|
||||
if chapter.number == currentChapter {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.caption.bold())
|
||||
.foregroundStyle(.amber)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.listRowBackground(
|
||||
chapter.number == currentChapter
|
||||
? Color.amber.opacity(0.1)
|
||||
: Color.clear
|
||||
)
|
||||
.id(chapter.number)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.onAppear {
|
||||
// Scroll to current chapter on appear
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
withAnimation {
|
||||
proxy.scrollTo(currentChapter, anchor: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Chapters")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom seek slider
|
||||
// A thicker, rounded-thumb slider that matches the amber design language.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user