621 lines
24 KiB
Swift
621 lines
24 KiB
Swift
import SwiftUI
|
|
import WidgetKit
|
|
import AppIntents
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// NowPlayingWidgetViews.swift
|
|
// TARGET: Widget extension only.
|
|
//
|
|
// v2 redesign: glassmorphism, waveform scrubber, color-adaptive theming,
|
|
// crossfade countdown, Up Next queue (large).
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// MARK: - Color Helpers
|
|
|
|
extension Color {
|
|
/// Create a Color from a hex string like "E74C3C".
|
|
init(hex: String) {
|
|
var h = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if h.hasPrefix("#") { h = String(h.dropFirst()) }
|
|
guard h.count == 6, let val = UInt64(h, radix: 16) else {
|
|
self = .pink
|
|
return
|
|
}
|
|
self.init(
|
|
red: Double((val >> 16) & 0xFF) / 255.0,
|
|
green: Double((val >> 8) & 0xFF) / 255.0,
|
|
blue: Double( val & 0xFF) / 255.0
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Adaptive Colors from Entry
|
|
|
|
struct WidgetColors {
|
|
let accent: Color
|
|
let secondary: Color
|
|
let textPrimary: Color
|
|
let textSecondary: Color
|
|
let textTertiary: Color
|
|
let barUnplayed: Color
|
|
|
|
init(entry: NowPlayingEntry) {
|
|
if let hex = entry.accentColorHex {
|
|
accent = Color(hex: hex)
|
|
} else {
|
|
accent = Color(red: 0.91, green: 0.30, blue: 0.24) // default pink-red
|
|
}
|
|
if let hex = entry.secondaryColorHex {
|
|
secondary = Color(hex: hex)
|
|
} else {
|
|
secondary = .white.opacity(0.7)
|
|
}
|
|
textPrimary = .white
|
|
textSecondary = secondary.opacity(0.9)
|
|
textTertiary = .white.opacity(0.4)
|
|
barUnplayed = .white.opacity(0.18)
|
|
}
|
|
}
|
|
|
|
// MARK: - Main View (dispatches by family)
|
|
|
|
struct NowPlayingWidgetView: View {
|
|
let entry: NowPlayingEntry
|
|
@Environment(\.widgetFamily) var family
|
|
|
|
var body: some View {
|
|
let colors = WidgetColors(entry: entry)
|
|
|
|
ZStack {
|
|
// Layer 0: Blurred album art background
|
|
backgroundLayer
|
|
|
|
// Dark scrim for contrast
|
|
Color.black.opacity(0.28)
|
|
|
|
// Layer 1: Glass panel + content
|
|
switch family {
|
|
case .systemSmall:
|
|
SmallContent(entry: entry, colors: colors)
|
|
case .systemMedium:
|
|
MediumContent(entry: entry, colors: colors)
|
|
case .systemLarge:
|
|
LargeContent(entry: entry, colors: colors)
|
|
default:
|
|
MediumContent(entry: entry, colors: colors)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var backgroundLayer: some View {
|
|
if let data = entry.blurredArtData, let img = UIImage(data: data) {
|
|
Image(uiImage: img)
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
} else {
|
|
LinearGradient(
|
|
colors: [Color(hex: entry.accentColorHex ?? "3A1A1A"), .black],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// MARK: - Glass Panel Container
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
struct GlassPanel<Content: View>: View {
|
|
let colors: WidgetColors
|
|
@ViewBuilder var content: () -> Content
|
|
|
|
var body: some View {
|
|
content()
|
|
.padding(12)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
.fill(.ultraThinMaterial)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
.fill(colors.accent.opacity(0.06))
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
.stroke(.white.opacity(0.12), lineWidth: 0.5)
|
|
)
|
|
)
|
|
.padding(8)
|
|
}
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// MARK: - Waveform Bar (Canvas + SeekToIntent overlay)
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
struct WaveformBar: View {
|
|
let samples: [Float]
|
|
let progress: Double
|
|
let accentColor: Color
|
|
let unplayedColor: Color
|
|
let barHeight: CGFloat
|
|
|
|
private let barCount = 40
|
|
private let seekSegments = 20
|
|
|
|
var body: some View {
|
|
GeometryReader { geo in
|
|
let playedCount = Int(Double(barCount) * progress)
|
|
|
|
ZStack {
|
|
// Waveform canvas
|
|
Canvas { ctx, size in
|
|
let effectiveSamples = samples.isEmpty
|
|
? (0..<barCount).map { _ in Float(0.3) }
|
|
: samples
|
|
|
|
let totalBars = min(effectiveSamples.count, barCount)
|
|
let spacing: CGFloat = 1.5
|
|
let bw = max((size.width - CGFloat(totalBars - 1) * spacing) / CGFloat(totalBars), 1.5)
|
|
|
|
for i in 0..<totalBars {
|
|
let amp = CGFloat(effectiveSamples[i])
|
|
let h = max(amp * (barHeight - 2), 2)
|
|
let x = CGFloat(i) * (bw + spacing)
|
|
let y = barHeight - h
|
|
|
|
let rect = CGRect(x: x, y: y, width: bw, height: h)
|
|
let rr = Path(roundedRect: rect, cornerRadius: 1)
|
|
|
|
let color: Color = i < playedCount ? accentColor : unplayedColor
|
|
ctx.fill(rr, with: .color(color))
|
|
}
|
|
}
|
|
.frame(height: barHeight)
|
|
|
|
// Seek tap zones — 20 segments over the waveform
|
|
HStack(spacing: 0) {
|
|
ForEach(0..<seekSegments, id: \.self) { i in
|
|
let fraction = (Double(i) + 0.5) / Double(seekSegments)
|
|
Button(intent: SeekToIntent(fraction: fraction)) {
|
|
Color.clear
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.frame(height: max(barHeight, 28))
|
|
}
|
|
}
|
|
.frame(height: max(barHeight, 28))
|
|
}
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// MARK: - Transport Controls
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
struct TransportControls: View {
|
|
let isPlaying: Bool
|
|
let iconSize: CGFloat
|
|
let playSize: CGFloat
|
|
|
|
var body: some View {
|
|
HStack(spacing: iconSize * 1.5) {
|
|
// Previous
|
|
Button(intent: PreviousTrackIntent()) {
|
|
Image(systemName: "backward.fill")
|
|
.font(.system(size: iconSize, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.85))
|
|
}
|
|
.buttonStyle(.plain)
|
|
|
|
// Play / Pause
|
|
Button(intent: PlayPauseIntent()) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(.white.opacity(0.15))
|
|
.frame(width: playSize, height: playSize)
|
|
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
|
|
.font(.system(size: playSize * 0.4, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
.offset(x: isPlaying ? 0 : 1) // optical center for play triangle
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
|
|
// Next
|
|
Button(intent: NextTrackIntent()) {
|
|
Image(systemName: "forward.fill")
|
|
.font(.system(size: iconSize, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.85))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// MARK: - Cover Art View
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
struct CoverArtView: View {
|
|
let data: Data?
|
|
let size: CGFloat
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let data = data, let img = UIImage(data: data) {
|
|
Image(uiImage: img)
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
} else {
|
|
ZStack {
|
|
Color.white.opacity(0.08)
|
|
Image(systemName: "music.note")
|
|
.font(.system(size: size * 0.35, weight: .light))
|
|
.foregroundStyle(.white.opacity(0.3))
|
|
}
|
|
}
|
|
}
|
|
.frame(width: size, height: size)
|
|
.clipShape(RoundedRectangle(cornerRadius: size * 0.15, style: .continuous))
|
|
.shadow(color: .black.opacity(0.35), radius: 6, y: 3)
|
|
}
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// MARK: - SMALL WIDGET
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
struct SmallContent: View {
|
|
let entry: NowPlayingEntry
|
|
let colors: WidgetColors
|
|
|
|
var body: some View {
|
|
GlassPanel(colors: colors) {
|
|
VStack(spacing: 6) {
|
|
// Top: art + text
|
|
HStack(spacing: 10) {
|
|
CoverArtView(data: entry.coverArtData, size: 48)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(entry.songTitle)
|
|
.font(.system(size: 13, weight: .bold))
|
|
.foregroundStyle(colors.textPrimary)
|
|
.lineLimit(1)
|
|
|
|
Text(entry.artist)
|
|
.font(.system(size: 11, weight: .medium))
|
|
.foregroundStyle(colors.textSecondary)
|
|
.lineLimit(1)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
// Waveform
|
|
VStack(spacing: 3) {
|
|
WaveformBar(
|
|
samples: entry.waveformSamples,
|
|
progress: entry.progress,
|
|
accentColor: colors.accent,
|
|
unplayedColor: colors.barUnplayed,
|
|
barHeight: 18
|
|
)
|
|
|
|
HStack {
|
|
Text(entry.currentTimeFormatted)
|
|
.font(.system(size: 9, weight: .medium).monospacedDigit())
|
|
.foregroundStyle(colors.textTertiary)
|
|
Spacer()
|
|
Text(entry.remainingTimeFormatted)
|
|
.font(.system(size: 9, weight: .medium).monospacedDigit())
|
|
.foregroundStyle(colors.textTertiary)
|
|
}
|
|
}
|
|
|
|
// Controls
|
|
TransportControls(isPlaying: entry.isPlaying, iconSize: 14, playSize: 28)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// MARK: - MEDIUM WIDGET
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
struct MediumContent: View {
|
|
let entry: NowPlayingEntry
|
|
let colors: WidgetColors
|
|
|
|
var body: some View {
|
|
GlassPanel(colors: colors) {
|
|
VStack(spacing: 6) {
|
|
// Top row: art + song info
|
|
HStack(spacing: 14) {
|
|
CoverArtView(data: entry.coverArtData, size: 64)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(entry.songTitle)
|
|
.font(.system(size: 15, weight: .bold))
|
|
.foregroundStyle(colors.textPrimary)
|
|
.lineLimit(1)
|
|
|
|
Text(entry.artist)
|
|
.font(.system(size: 12, weight: .medium))
|
|
.foregroundStyle(colors.textSecondary)
|
|
.lineLimit(1)
|
|
|
|
Text(entry.album)
|
|
.font(.system(size: 11, weight: .regular))
|
|
.foregroundStyle(colors.textTertiary)
|
|
.lineLimit(1)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
// Waveform + times
|
|
VStack(spacing: 3) {
|
|
WaveformBar(
|
|
samples: entry.waveformSamples,
|
|
progress: entry.progress,
|
|
accentColor: colors.accent,
|
|
unplayedColor: colors.barUnplayed,
|
|
barHeight: 18
|
|
)
|
|
|
|
HStack {
|
|
Text(entry.currentTimeFormatted)
|
|
.font(.system(size: 9, weight: .medium).monospacedDigit())
|
|
.foregroundStyle(colors.textTertiary)
|
|
Spacer()
|
|
Text(entry.remainingTimeFormatted)
|
|
.font(.system(size: 9, weight: .medium).monospacedDigit())
|
|
.foregroundStyle(colors.textTertiary)
|
|
}
|
|
}
|
|
|
|
// Transport controls
|
|
TransportControls(isPlaying: entry.isPlaying, iconSize: 16, playSize: 30)
|
|
|
|
// Up Next footer with crossfade countdown
|
|
upNextFooter
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var upNextFooter: some View {
|
|
let nextTrack = entry.queueNext.first
|
|
|
|
if nextTrack != nil || entry.crossfadeCountdownFormatted != nil {
|
|
HStack(spacing: 6) {
|
|
Text("♪")
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(.white.opacity(0.4))
|
|
|
|
if let track = nextTrack {
|
|
Text("Up Next: \(track.title) · \(track.artist)")
|
|
.font(.system(size: 10, weight: .regular))
|
|
.foregroundStyle(.white.opacity(0.4))
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
}
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
if entry.isPlaying, let cd = entry.crossfadeCountdownFormatted {
|
|
Text(cd)
|
|
.font(.system(size: 9, weight: .semibold).monospacedDigit())
|
|
.foregroundStyle(colors.accent.opacity(0.8))
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
|
.fill(.white.opacity(0.06))
|
|
)
|
|
} else if !entry.isPlaying {
|
|
Text("paused")
|
|
.font(.system(size: 9, weight: .semibold))
|
|
.foregroundStyle(colors.accent.opacity(0.7))
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
|
.fill(.white.opacity(0.06))
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// MARK: - LARGE WIDGET
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
struct LargeContent: View {
|
|
let entry: NowPlayingEntry
|
|
let colors: WidgetColors
|
|
|
|
var body: some View {
|
|
GlassPanel(colors: colors) {
|
|
VStack(spacing: 8) {
|
|
// Header: art + info + waveform
|
|
HStack(spacing: 16) {
|
|
CoverArtView(data: entry.coverArtData, size: 80)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(entry.songTitle)
|
|
.font(.system(size: 17, weight: .bold))
|
|
.foregroundStyle(colors.textPrimary)
|
|
.lineLimit(1)
|
|
|
|
Text(entry.artist)
|
|
.font(.system(size: 13, weight: .medium))
|
|
.foregroundStyle(colors.textSecondary)
|
|
.lineLimit(1)
|
|
|
|
Text(entry.album)
|
|
.font(.system(size: 11, weight: .regular))
|
|
.foregroundStyle(colors.textTertiary)
|
|
.lineLimit(1)
|
|
|
|
Spacer(minLength: 4)
|
|
|
|
// Waveform inside the header
|
|
VStack(spacing: 3) {
|
|
WaveformBar(
|
|
samples: entry.waveformSamples,
|
|
progress: entry.progress,
|
|
accentColor: colors.accent,
|
|
unplayedColor: colors.barUnplayed,
|
|
barHeight: 22
|
|
)
|
|
|
|
HStack {
|
|
Text(entry.currentTimeFormatted)
|
|
.font(.system(size: 9, weight: .medium).monospacedDigit())
|
|
.foregroundStyle(colors.textTertiary)
|
|
Spacer()
|
|
Text(entry.remainingTimeFormatted)
|
|
.font(.system(size: 9, weight: .medium).monospacedDigit())
|
|
.foregroundStyle(colors.textTertiary)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
// Transport controls
|
|
TransportControls(isPlaying: entry.isPlaying, iconSize: 18, playSize: 34)
|
|
|
|
// Divider
|
|
Rectangle()
|
|
.fill(.white.opacity(0.08))
|
|
.frame(height: 0.5)
|
|
.padding(.horizontal, 2)
|
|
|
|
// Queue section
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text("UP NEXT")
|
|
.font(.system(size: 10, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.3))
|
|
.tracking(0.8)
|
|
.padding(.bottom, 6)
|
|
.padding(.leading, 2)
|
|
|
|
if entry.queueNext.isEmpty {
|
|
Text("No upcoming tracks")
|
|
.font(.system(size: 12))
|
|
.foregroundStyle(.white.opacity(0.25))
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.padding(.vertical, 8)
|
|
} else {
|
|
ForEach(Array(entry.queueNext.prefix(3).enumerated()), id: \.offset) { idx, item in
|
|
queueRow(index: idx + 1, item: item)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxHeight: .infinity, alignment: .top)
|
|
|
|
// Crossfade footer
|
|
crossfadeFooter
|
|
}
|
|
}
|
|
}
|
|
|
|
private func queueRow(index: Int, item: WidgetQueueItem) -> some View {
|
|
HStack(spacing: 10) {
|
|
Text("\(index)")
|
|
.font(.system(size: 11, weight: .medium).monospacedDigit())
|
|
.foregroundStyle(.white.opacity(0.3))
|
|
.frame(width: 14, alignment: .center)
|
|
|
|
// Mini art placeholder (no per-track art data in widget)
|
|
RoundedRectangle(cornerRadius: 5, style: .continuous)
|
|
.fill(colors.accent.opacity(0.2))
|
|
.frame(width: 32, height: 32)
|
|
.overlay(
|
|
Image(systemName: "music.note")
|
|
.font(.system(size: 11, weight: .light))
|
|
.foregroundStyle(.white.opacity(0.2))
|
|
)
|
|
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
Text(item.title)
|
|
.font(.system(size: 12, weight: .medium))
|
|
.foregroundStyle(.white.opacity(0.8))
|
|
.lineLimit(1)
|
|
|
|
Text(item.artist)
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(.white.opacity(0.4))
|
|
.lineLimit(1)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.padding(.vertical, 4)
|
|
.padding(.horizontal, 6)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.fill(.white.opacity(0.03))
|
|
)
|
|
.padding(.bottom, 2)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var crossfadeFooter: some View {
|
|
if entry.isPlaying, let cd = entry.crossfadeCountdownFormatted {
|
|
HStack(spacing: 4) {
|
|
Text("⤳")
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(.white.opacity(0.4))
|
|
Text("Crossfade \(cd)")
|
|
.font(.system(size: 10, weight: .medium))
|
|
.foregroundStyle(colors.accent.opacity(0.7))
|
|
.lineLimit(1)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.leading, 2)
|
|
} else if !entry.isPlaying {
|
|
HStack(spacing: 4) {
|
|
Text("⏸")
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(.white.opacity(0.4))
|
|
Text("Paused")
|
|
.font(.system(size: 10, weight: .medium))
|
|
.foregroundStyle(colors.accent.opacity(0.7))
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.leading, 2)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
#if DEBUG
|
|
#Preview("Small", as: .systemSmall) {
|
|
NowPlayingWidget()
|
|
} timeline: {
|
|
NowPlayingEntry.placeholder
|
|
}
|
|
|
|
#Preview("Medium", as: .systemMedium) {
|
|
NowPlayingWidget()
|
|
} timeline: {
|
|
NowPlayingEntry.placeholder
|
|
}
|
|
|
|
#Preview("Large", as: .systemLarge) {
|
|
NowPlayingWidget()
|
|
} timeline: {
|
|
NowPlayingEntry.placeholder
|
|
}
|
|
#endif
|