PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Modules / FluentPlayer / ProductVideoRenderer.php

ProductVideoRenderer.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Modules/FluentPlayer/ProductVideoRenderer.php

260 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\FluentPlayer;
4
5 use FluentCart\App\Models\Product;
6 use FluentCart\App\Vite;
7
8 /**
9 * Gallery markup for a product's FluentPlayer videos: one thumbnail in the
10 * strip and one hidden inline player per visible media.
11 *
12 * The strip is interleaved with the images: each video carries the number of
13 * images it sits behind (ProductVideoSettings), so the caller renders the
14 * videos due before image N, then image N, and flushes the rest at the end.
15 */
16 class ProductVideoRenderer
17 {
18 protected $product;
19
20 protected $config;
21
22 protected $videos = null;
23
24 protected $showFirstByDefault = false;
25
26 /** @var array<int, bool> media ids already written into the strip. */
27 protected $renderedThumbs = [];
28
29 public function __construct(Product $product)
30 {
31 $this->product = $product;
32 $this->config = FluentPlayerBridge::isActive() ? ProductVideoSettings::fromProduct($product) : null;
33 }
34
35 public function getMediaIds(): array
36 {
37 return $this->config ? $this->config['media_ids'] : [];
38 }
39
40 /**
41 * Only media that is both visible and rendered by FluentPlayer: a thumb
42 * without a player would leave the gallery blank when clicked.
43 *
44 * @return array<int, array> media id => summary + 'player' html.
45 */
46 public function getVideos(): array
47 {
48 if ($this->videos !== null) {
49 return $this->videos;
50 }
51
52 $this->videos = [];
53 if ($this->config === null || !$this->isFullPageRender()) {
54 return $this->videos;
55 }
56
57 foreach ($this->getMediaIds() as $mediaId) {
58 $media = FluentPlayerBridge::findVisibleMedia($mediaId);
59 if (!$media) {
60 continue;
61 }
62
63 $player = FluentPlayerBridge::renderPlayer($mediaId, 'fct-product-video-player');
64 if ($player === '') {
65 continue;
66 }
67
68 $this->videos[$mediaId] = FluentPlayerBridge::summarize($media) + ['player' => $player];
69 }
70
71 return $this->videos;
72 }
73
74 public function isAvailable(): bool
75 {
76 return count($this->getVideos()) > 0;
77 }
78
79 /**
80 * A product without gallery images opens on its first video instead of a
81 * placeholder image, so the video is visible without a click.
82 */
83 public function showFirstByDefault(bool $show): void
84 {
85 $this->showFirstByDefault = $show;
86 }
87
88 /**
89 * @return array<int, int|null> media id => images that precede it (null = last).
90 */
91 public function getPositions(): array
92 {
93 return $this->config ? ProductVideoSettings::positionMap($this->config) : [];
94 }
95
96 /**
97 * The video the page opens on: the one the admin dragged in front of every
98 * image, or — for a product with no image at all — the first video.
99 */
100 public function getDefaultMediaId(): int
101 {
102 $videos = $this->getVideos();
103 if (!$videos) {
104 return 0;
105 }
106
107 $positions = $this->getPositions();
108 foreach (array_keys($videos) as $mediaId) {
109 if (isset($positions[$mediaId]) && $positions[$mediaId] === 0) {
110 return (int) $mediaId;
111 }
112 }
113
114 if (!$this->showFirstByDefault) {
115 return 0;
116 }
117
118 $ids = array_keys($videos);
119
120 return (int) $ids[0];
121 }
122
123 /**
124 * FluentPlayer's player config is only printed by the normal page footer, so
125 * admin-ajax / REST fragments (shop quick-view) must not carry a player.
126 */
127 protected function isFullPageRender(): bool
128 {
129 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
130 return false;
131 }
132
133 if (defined('REST_REQUEST') && REST_REQUEST) {
134 return false;
135 }
136
137 return true;
138 }
139
140 protected function getVideoTitle(array $video): string
141 {
142 $title = trim((string) $video['title']);
143
144 return $title !== '' ? $title : (string) $this->product->post_title;
145 }
146
147 /**
148 * Never the product photo: a neutral placeholder when FluentPlayer has no poster.
149 */
150 protected function getPosterUrl(array $video): string
151 {
152 $poster = (string) $video['poster'];
153
154 return $poster !== '' ? $poster : Vite::getAssetUrl('images/placeholder.svg');
155 }
156
157 protected function playIconSvg(): string
158 {
159 return '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false" width="24" height="24"><path d="M8 5v14l11-7z"/></svg>';
160 }
161
162 /**
163 * The video thumbs due before the image about to be rendered: every video
164 * the admin placed after `$imageIndex` images or fewer. Videos with no
165 * position wait for the closing renderThumbControls() flush.
166 */
167 public function renderThumbControlsBefore(int $imageIndex, bool $claimFocus = false): void
168 {
169 $positions = $this->getPositions();
170
171 foreach ($this->getVideos() as $mediaId => $video) {
172 if (isset($this->renderedThumbs[$mediaId])) {
173 continue;
174 }
175 $position = isset($positions[$mediaId]) ? $positions[$mediaId] : null;
176 if ($position === null || $position > $imageIndex) {
177 continue;
178 }
179 $this->renderThumbControl((int) $mediaId, $video, $claimFocus);
180 $claimFocus = false;
181 }
182 }
183
184 /**
185 * Every video thumb not written yet — the ones placed after the last image
186 * and, when the strip was cut short by a thumbnail limit, the rest.
187 */
188 public function renderThumbControls(bool $claimFocus = false): void
189 {
190 foreach ($this->getVideos() as $mediaId => $video) {
191 if (isset($this->renderedThumbs[$mediaId])) {
192 continue;
193 }
194 $this->renderThumbControl((int) $mediaId, $video, $claimFocus);
195 $claimFocus = false;
196 }
197 }
198
199 /**
200 * Shares the thumb-control contract so keyboard navigation and variation
201 * filtering treat video thumbs like images; ImageGallery.js keys on
202 * data-fct-video-media-id.
203 *
204 * The strip uses a roving tabindex. When no image thumb holds the slot
205 * (a video-only product) the first video thumb takes it, otherwise the
206 * strip could never be reached with the keyboard. The default video's
207 * thumb is also selected, mirroring the default image thumb.
208 */
209 protected function renderThumbControl(int $mediaId, array $video, bool $claimFocus): void
210 {
211 $this->renderedThumbs[$mediaId] = true;
212
213 $defaultId = $this->getDefaultMediaId();
214 $title = $this->getVideoTitle($video);
215 $isSelected = $mediaId === $defaultId;
216 $tabindex = ($claimFocus || $isSelected) ? '0' : '-1';
217 ?>
218 <button
219 type="button"
220 class="fct-gallery-thumb-control-button fct-gallery-video-thumb<?php echo $isSelected ? ' active' : ''; ?>"
221 data-fluent-cart-thumb-control-button
222 data-variation-id="0"
223 data-fct-video-media-id="<?php echo esc_attr((string) $mediaId); ?>"
224 aria-label="<?php
225 /* translators: %1$s: video title */
226 echo esc_attr(sprintf(__('Play video: %1$s', 'fluent-cart'), $title));
227 ?>"
228 aria-pressed="<?php echo $isSelected ? 'true' : 'false'; ?>"
229 tabindex="<?php echo esc_attr($tabindex); ?>"
230 >
231 <img
232 class="fct-gallery-control-thumb"
233 src="<?php echo esc_url($this->getPosterUrl($video)); ?>"
234 alt=""
235 />
236 <span class="fct-gallery-video-thumb__badge" aria-hidden="true"><?php echo $this->playIconSvg(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG ?></span>
237 </button>
238 <?php
239 }
240
241 /**
242 * Hidden until ImageGallery.js marks the matching container `is-active`,
243 * except the default video, which is active from the start.
244 */
245 public function renderInlinePlayers(): void
246 {
247 $defaultId = $this->getDefaultMediaId();
248
249 foreach ($this->getVideos() as $mediaId => $video) {
250 ?>
251 <div class="fct-product-gallery-video<?php echo $mediaId === $defaultId ? ' is-active' : ''; ?>"
252 data-fct-product-gallery-video
253 data-fct-video-media-id="<?php echo esc_attr((string) $mediaId); ?>">
254 <?php echo $video['player']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FluentPlayer renders and escapes its own player markup ?>
255 </div>
256 <?php
257 }
258 }
259 }
260