plac-for-apple-platform

Unofficial Roon client for Apple devices

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
// Copyright 2025 Shota FUJI
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

import PlacKit
import SwiftUI

struct PlaybackBar<TZone: Zone & Identifiable>: View {
	@Binding var zone_id: String?

	var zones: [String: TZone] = [:]

	private var zone: TZone? {
		zones.first(where: { _, zone in
			zone.id == zone_id
		})?.value
	}

	var body: some View {
		VStack(spacing: 12) {
			VStack {
				if let line1 = zone?.nowPlaying?.twoline.0 {
					Text(line1)
						.font(.headline)
						.lineLimit(1)
						.frame(maxWidth: .infinity, alignment: .leading)
				} else {
					Text("Not Playing")
						.font(.subheadline)
						.foregroundStyle(.secondary)
						.lineLimit(1)
						.frame(maxWidth: .infinity, alignment: .leading)
				}

				Text(zone?.nowPlaying?.twoline.1 ?? " ")
					.font(.subheadline)
					.lineLimit(1)
					.frame(maxWidth: .infinity, alignment: .leading)
			}

			HStack {
				HStack {
					if zone?.playback == PlacKit.PLAC_TRANSPORT_PLAYBACK_PLAYING {
						Button {
						} label: {
							Label("Pause", systemImage: "pause.fill")
						}
					} else {
						Button {
						} label: {
							Label("Play", systemImage: "play.fill")
						}
					}
				}
				.labelStyle(.iconOnly)
				.buttonStyle(.borderless)
				.font(.system(size: 20))

				Spacer()

				if zones.count > 0 && zone_id != nil {
					Picker("Zone", selection: $zone_id) {
						ForEach(
							zones.values.sorted(by: {
								$0.id < $1.id
							})
						) { zone in
							Text(zone.name).tag(zone.id as String?)
						}
					}
					.scaledToFit()
				}
			}
		}
		.padding([.horizontal], 10)
		.padding([.vertical], 6)
	}
}

#Preview("PlaybackBar") {
	@Previewable @State var zone_id: String? = "foo"

	var zones: [String: MockZone] = [
		"foo": MockZone(
			id: "foo",
			name: "Foo",
			playback: PlacKit.PLAC_TRANSPORT_PLAYBACK_PLAYING,
			nowPlaying: MockNowPlaying(
				line1: "My great song",
				line2: "Some untrusted artist",
				seek: (5, 3 * 60 + 30)
			)
		),
		"bar": MockZone(
			id: "bar",
			name: "Bar",
			playback: PlacKit.PLAC_TRANSPORT_PLAYBACK_STOPPED
		),
	]

	PlaybackBar(zone_id: $zone_id, zones: zones)
}