PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / src / admin / dashboard / components / Onboarding / Integrations.js
presto-player / src / admin / dashboard / components / Onboarding Last commit date
Done.js 1 month ago Integrations.js 1 month ago PremiumFeatures.js 1 month ago UserInfo.js 1 month ago Welcome.js 1 month ago
Integrations.js
322 lines
1 import { useState, useEffect, useRef } from "react";
2 import {
3 Button,
4 Title,
5 Text,
6 Checkbox,
7 Badge,
8 Loader,
9 Container,
10 } from "@bsf/force-ui";
11 import { ChevronRight, ChevronLeft } from "lucide-react";
12 import { toast } from "@bsf/force-ui";
13 import {
14 checkPluginStatus,
15 installPlugin,
16 activatePluginBySlug,
17 } from "../../utils/pluginUtils";
18
19 const { __, sprintf } = wp.i18n;
20
21 const prestoData = window.prestoPlayer || {};
22 const addonList = prestoData?.onboarding?.addonList || [];
23
24 const AddonItem = ({ addon, status, isSelected, onToggle, processing }) => {
25 const statusLabels = {
26 active: __("Active", "presto-player"),
27 inactive: processing
28 ? __("Activating\u2026", "presto-player")
29 : __("Inactive", "presto-player"),
30 default: processing ? __("Installing\u2026", "presto-player") : "",
31 };
32
33 const statusVariants = {
34 active: "green",
35 inactive: "yellow",
36 default: "neutral",
37 };
38
39 const loaderClasses = {
40 active: "text-badge-color-green",
41 inactive: "text-badge-color-yellow",
42 default: "text-badge-color-gray",
43 };
44
45 const showLoader = isSelected && processing;
46
47 return (
48 <div className="p-3 bg-background-primary flex gap-2 rounded-md shadow-sm [&>div]:flex-row-reverse [&>div]:w-full [&>div]:justify-between">
49 <span className="w-6 h-6 mt-1 flex-shrink-0">
50 {addon?.logo && (
51 <img
52 className="w-full h-full rounded"
53 src={addon.logo}
54 alt={sprintf(
55 /* translators: %s is the addon title */
56 __("%s logo", "presto-player"),
57 addon.title
58 )}
59 />
60 )}
61 </span>
62
63 <Checkbox
64 label={{
65 description: (
66 <Text
67 size={14}
68 weight={400}
69 color="secondary"
70 className="!text-text-secondary"
71 >
72 {addon.description}
73 </Text>
74 ),
75 heading: (
76 <span className="flex items-center gap-2">
77 <span className="cursor-pointer">{addon.title}</span>
78 {(status || showLoader) && (
79 <Badge
80 size="xs"
81 icon={
82 showLoader && (
83 <Loader
84 className={`[&>svg]:size-3.5 ${
85 loaderClasses[status] || loaderClasses.default
86 }`}
87 />
88 )
89 }
90 variant={statusVariants[status] || statusVariants.default}
91 label={statusLabels[status] || statusLabels.default}
92 />
93 )}
94 </span>
95 ),
96 }}
97 size="sm"
98 checked={isSelected}
99 onChange={(checked) => onToggle(addon.slug, checked)}
100 onKeyDown={(event) => {
101 if (event.key === "Enter") {
102 event.preventDefault();
103 onToggle(addon.slug, !isSelected);
104 }
105 }}
106 className="focus:border-border-interactive [&_p]:text-text-secondary"
107 />
108 </div>
109 );
110 };
111
112 const Integrations = ({ goToNextStep, goToPreviousStep }) => {
113 const [addonStatus, setAddonStatus] = useState({});
114 const [selectedAddons, setSelectedAddons] = useState([]);
115 const [processing, setProcessing] = useState(false);
116 const timerRef = useRef(null);
117
118 // Fetch addon statuses on mount.
119 useEffect(() => {
120 const fetchStatuses = async () => {
121 if (!addonList.length) return;
122
123 const results = await Promise.all(
124 addonList.map(async (addon) => {
125 try {
126 const result = await checkPluginStatus(addon.slug);
127 if (!result.success) return null;
128
129 const { installed, active } = result.data;
130 let status = "";
131 if (active) status = "active";
132 else if (installed) status = "inactive";
133
134 return { slug: addon.slug, status };
135 } catch (error) {
136 return null;
137 }
138 })
139 );
140
141 const statuses = {};
142 const initialSelected = [];
143
144 results.forEach((result) => {
145 if (result) {
146 statuses[result.slug] = result.status;
147 // Check already-active plugins. Leave not-installed ones unchecked.
148 if (result.status === "active") {
149 initialSelected.push(result.slug);
150 }
151 }
152 });
153
154 setAddonStatus(statuses);
155 setSelectedAddons(initialSelected);
156 };
157
158 fetchStatuses();
159 }, []);
160
161 useEffect(() => {
162 return () => clearTimeout(timerRef.current);
163 }, []);
164
165 const handleAddonToggle = (addonSlug, checked) => {
166 setSelectedAddons((prev) =>
167 checked
168 ? prev.includes(addonSlug)
169 ? prev
170 : [...prev, addonSlug]
171 : prev.filter((slug) => slug !== addonSlug)
172 );
173 };
174
175 const handleContinue = async () => {
176 if (processing) return;
177
178 const allActive = selectedAddons.every(
179 (slug) => addonStatus[slug] === "active"
180 );
181
182 if (selectedAddons.length === 0 || allActive) {
183 goToNextStep();
184 return;
185 }
186
187 setProcessing(true);
188
189 try {
190 for (const addon of addonList) {
191 const addonSlug = addon.slug;
192
193 if (!selectedAddons.includes(addonSlug)) continue;
194
195 setProcessing(addonSlug);
196
197 const currentStatus = addonStatus[addonSlug];
198
199 if (currentStatus === "active") continue;
200
201 try {
202 // If not installed, install first.
203 if (!currentStatus) {
204 const installResult = await installPlugin(addonSlug);
205 if (!installResult.success) {
206 throw new Error(installResult.error);
207 }
208 }
209
210 // Activate (whether freshly installed or already inactive).
211 if (currentStatus === "inactive" || !currentStatus) {
212 const activateResult = await activatePluginBySlug(addonSlug);
213 if (!activateResult.success) {
214 throw new Error(activateResult.error);
215 }
216 }
217
218 // Update status to active.
219 setAddonStatus((prev) => ({
220 ...prev,
221 [addonSlug]: "active",
222 }));
223 } catch (error) {
224 toast.error(__("Error!", "presto-player"), {
225 description: sprintf(
226 /* translators: %s is the addon title */
227 __("Failed to set up %s.", "presto-player"),
228 addon.title
229 ),
230 });
231 }
232 }
233
234 // Navigate after short delay for status badge to update visually.
235 timerRef.current = setTimeout(goToNextStep, 500);
236 } catch (error) {
237 toast.error(__("Error!", "presto-player"), {
238 description:
239 error.message || __("Failed to setup addons.", "presto-player"),
240 });
241 } finally {
242 setProcessing(false);
243 }
244 };
245
246 return (
247 <Container direction="column" gap="none">
248 <Container direction="column" gap="xs">
249 <Title
250 size="lg"
251 tag="h3"
252 title={__("Integrations and features", "presto-player")}
253 />
254 <Text size={14} weight={400} color="secondary" className="max-w-[35rem]">
255 {__(
256 "To help you get the most out of Presto Player, we recommend adding these powerful integrations:",
257 "presto-player"
258 )}
259 </Text>
260 </Container>
261
262 <div className="p-2 bg-background-secondary flex flex-col gap-1 rounded-lg overflow-auto mt-8">
263 {addonList.map((addon, index) => (
264 <AddonItem
265 key={addon.slug || index}
266 addon={addon}
267 status={addonStatus[addon.slug] || ""}
268 isSelected={selectedAddons.includes(addon.slug)}
269 onToggle={handleAddonToggle}
270 processing={processing === addon.slug}
271 />
272 ))}
273 </div>
274
275 <Container
276 direction="row"
277 align="center"
278 className={`justify-between mt-8 ${
279 processing ? "opacity-50 pointer-events-none" : ""
280 }`}
281 >
282 <Button
283 icon={<ChevronLeft />}
284 variant="outline"
285 onClick={goToPreviousStep}
286 disabled={!!processing}
287 >
288 {__("Back", "presto-player")}
289 </Button>
290 <Container direction="row" align="center" gap="sm">
291 <Button
292 className="text-text-tertiary"
293 variant="ghost"
294 onClick={goToNextStep}
295 disabled={!!processing}
296 >
297 {__("Skip", "presto-player")}
298 </Button>
299 <Button
300 className={processing ? "cursor-not-allowed" : ""}
301 icon={
302 processing ? (
303 <Loader size="sm" variant="secondary" />
304 ) : (
305 <ChevronRight />
306 )
307 }
308 iconPosition="right"
309 onClick={handleContinue}
310 >
311 {processing
312 ? __("Setting up\u2026", "presto-player")
313 : __("Continue", "presto-player")}
314 </Button>
315 </Container>
316 </Container>
317 </Container>
318 );
319 };
320
321 export default Integrations;
322