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 isFavorited = false
|
||||||
@State private var isLiked = false
|
@State private var isLiked = false
|
||||||
@State private var showingSpeedMenu = false
|
@State private var showingSpeedMenu = false
|
||||||
|
@State private var showingChaptersList = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -515,14 +516,15 @@ struct FullPlayerView: View {
|
|||||||
.disabled(true)
|
.disabled(true)
|
||||||
|
|
||||||
// Queue (show chapters list)
|
// Queue (show chapters list)
|
||||||
Button {} label: {
|
Button {
|
||||||
|
showingChaptersList = true
|
||||||
|
} label: {
|
||||||
Image(systemName: "list.bullet")
|
Image(systemName: "list.bullet")
|
||||||
.font(.system(size: 22))
|
.font(.system(size: 22))
|
||||||
.foregroundStyle(.white.opacity(0.7))
|
.foregroundStyle(.white.opacity(0.7))
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.disabled(true)
|
|
||||||
|
|
||||||
// Sleep timer (placeholder)
|
// Sleep timer (placeholder)
|
||||||
Button {} label: {
|
Button {} label: {
|
||||||
@@ -538,6 +540,31 @@ struct FullPlayerView: View {
|
|||||||
.padding(.bottom, 24)
|
.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 {
|
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
|
// MARK: - Custom seek slider
|
||||||
// A thicker, rounded-thumb slider that matches the amber design language.
|
// A thicker, rounded-thumb slider that matches the amber design language.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user