diff --git a/ios/LibNovel/LibNovel/Views/Player/PlayerViews.swift b/ios/LibNovel/LibNovel/Views/Player/PlayerViews.swift index 7e3754b..4065187 100644 --- a/ios/LibNovel/LibNovel/Views/Player/PlayerViews.swift +++ b/ios/LibNovel/LibNovel/Views/Player/PlayerViews.swift @@ -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.