@interware/content-generator 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/_chunks/{App-CTh6owSM.mjs → App-2eX-mcfo.mjs} +2 -2
  2. package/dist/_chunks/{App-ugsCmlsC.js → App-CvZirjX_.js} +2 -2
  3. package/dist/_chunks/{ImageStudioTabs-Umf3GODW.mjs → ImageStudioTabs-B6fkCK-o.mjs} +353 -21
  4. package/dist/_chunks/{ImageStudioTabs-BFEF9ggK.js → ImageStudioTabs-CNktoRax.js} +351 -19
  5. package/dist/_chunks/{Settings-Sk78JUBS.js → Settings-B-QPodCh.js} +169 -1
  6. package/dist/_chunks/{Settings-CdGW2O8h.mjs → Settings-CaGruvIm.mjs} +170 -2
  7. package/dist/_chunks/{StudioApp-DoC0z4eh.js → StudioApp-BDoB31L0.js} +2 -2
  8. package/dist/_chunks/{StudioApp-CrGn-9Gg.mjs → StudioApp-CF03znuL.mjs} +2 -2
  9. package/dist/_chunks/{index-D6UAvh53.mjs → index-DlemRDzs.mjs} +3 -3
  10. package/dist/_chunks/{index-C2s9Jhcc.js → index-DwHGS-BJ.js} +3 -3
  11. package/dist/admin/index.js +1 -1
  12. package/dist/admin/index.mjs +1 -1
  13. package/dist/admin/src/components/settings/DriveSettings.d.ts +77 -0
  14. package/dist/admin/src/features/drive/DriveTab.d.ts +42 -0
  15. package/dist/admin/src/features/image-studio/ImageStudioTabs.d.ts +3 -0
  16. package/dist/admin/src/features/library/FileCard.d.ts +5 -2
  17. package/dist/admin/src/features/library/LibraryTab.d.ts +5 -0
  18. package/dist/admin/src/features/library/driveSync.d.ts +41 -0
  19. package/dist/admin/src/features/templates/export/templateOrigin.d.ts +10 -0
  20. package/dist/admin/src/lib/use-cases/drive/index.d.ts +3 -0
  21. package/dist/server/index.js +1411 -49
  22. package/dist/server/index.mjs +1409 -47
  23. package/dist/server/src/bootstrap.d.ts +2 -0
  24. package/dist/server/src/content-types/drive-link/index.d.ts +62 -0
  25. package/dist/server/src/content-types/drive-link/schema.d.ts +68 -0
  26. package/dist/server/src/content-types/drive-op/index.d.ts +46 -0
  27. package/dist/server/src/content-types/drive-op/schema.d.ts +49 -0
  28. package/dist/server/src/content-types/index.d.ts +106 -0
  29. package/dist/server/src/controllers/drive.d.ts +30 -0
  30. package/dist/server/src/controllers/index.d.ts +130 -0
  31. package/dist/server/src/drive/client.d.ts +32 -0
  32. package/dist/server/src/drive/clientConfig.d.ts +27 -0
  33. package/dist/server/src/drive/common.d.ts +6 -0
  34. package/dist/server/src/drive/fakes.d.ts +104 -0
  35. package/dist/server/src/drive/files.d.ts +136 -0
  36. package/dist/server/src/drive/lifecycles.d.ts +7 -0
  37. package/dist/server/src/drive/links.d.ts +23 -0
  38. package/dist/server/src/drive/model.d.ts +99 -0
  39. package/dist/server/src/drive/oauth.d.ts +47 -0
  40. package/dist/server/src/drive/pull.d.ts +23 -0
  41. package/dist/server/src/drive/strapiPorts.d.ts +10 -0
  42. package/dist/server/src/drive/sync.d.ts +40 -0
  43. package/dist/server/src/index.d.ts +294 -0
  44. package/dist/server/src/services/drive.d.ts +141 -0
  45. package/dist/server/src/services/index.d.ts +55 -0
  46. package/package.json +1 -1
@@ -5,8 +5,8 @@ import { P as PluginProviders } from "./PluginProviders-DWMpcAIh.mjs";
5
5
  import { useMemo, useState, useEffect, useRef, useCallback } from "react";
6
6
  import { Box, Flex, Typography, Button, Badge, TextInput, Loader, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, TextButton, IconButton, Link, LinkButton, Field, Radio, Modal, Textarea, Combobox, ComboboxOption, Toggle, Grid } from "@strapi/design-system";
7
7
  import { Plus, Search, Check, Pencil, ExternalLink, ArrowLeft, Image, Clock, WarningCircle, Trash, Sparkle, PlusCircle } from "@strapi/icons";
8
- import { P as PLUGIN_ID, S as STUDIO_PATH, p as pluginPermissions } from "./index-D6UAvh53.mjs";
9
- import { L as LibraryImagePicker, P as PromptComposer, a as LEGACY_ROUTES, S as StudioRedirect, l as legacyRedirect } from "./ImageStudioTabs-Umf3GODW.mjs";
8
+ import { P as PLUGIN_ID, S as STUDIO_PATH, p as pluginPermissions } from "./index-DlemRDzs.mjs";
9
+ import { L as LibraryImagePicker, P as PromptComposer, a as LEGACY_ROUTES, S as StudioRedirect, l as legacyRedirect } from "./ImageStudioTabs-B6fkCK-o.mjs";
10
10
  import { useIntl } from "react-intl";
11
11
  import { useFormik } from "formik";
12
12
  import * as Yup from "yup";
@@ -7,8 +7,8 @@ const PluginProviders = require("./PluginProviders-D79TeGSE.js");
7
7
  const react = require("react");
8
8
  const designSystem = require("@strapi/design-system");
9
9
  const icons = require("@strapi/icons");
10
- const index = require("./index-C2s9Jhcc.js");
11
- const ImageStudioTabs = require("./ImageStudioTabs-BFEF9ggK.js");
10
+ const index = require("./index-DwHGS-BJ.js");
11
+ const ImageStudioTabs = require("./ImageStudioTabs-CNktoRax.js");
12
12
  const reactIntl = require("react-intl");
13
13
  const formik = require("formik");
14
14
  const Yup = require("yup");
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { Modal, Flex, Button, Loader, Typography, Grid, Box, lightTheme, Checkbox, Field, TextInput, Dialog, SingleSelect, SingleSelectOption, Link, Badge, IconButtonGroup, IconButton, Toggle, Menu, NumberInput, Divider, Textarea, Tabs } from "@strapi/design-system";
2
+ import { Modal, Flex, Button, Loader, Typography, Grid, Box, lightTheme, Checkbox, Field, TextInput, Dialog, SingleSelect, SingleSelectOption, Link, Badge, IconButtonGroup, IconButton, Toggle, Menu, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, TextButton, LinkButton, ProgressBar, NumberInput, Divider, Textarea, Tabs } from "@strapi/design-system";
3
3
  import { useLocation, Navigate, useSearchParams } from "react-router-dom";
4
- import { S as STUDIO_PATH } from "./index-D6UAvh53.mjs";
4
+ import { S as STUDIO_PATH } from "./index-DlemRDzs.mjs";
5
5
  import { useMemo, useState, useEffect, useId, useRef, useReducer, useCallback, useSyncExternalStore, useLayoutEffect, memo, forwardRef } from "react";
6
- import { ChevronRight, Folder, Magic, Pencil, Upload, Sparkle, Plus, Cross, Download, ArrowClockwise, House, ChevronDown, FilePdf, File as File$1, More, Trash, User, Star, SquaresFour, Quotes, PriceTag, Paragraph, NumberList, Minus, Italic, Image as Image$1, HeadingTwo, HeadingOne, Hashtag, Feather, Drag, BulletList, ArrowLeft, Check, ArrowsCounterClockwise, ArrowLineLeft, ArrowLineRight, Expand, WarningCircle, Play, Phone, Mail, PinMap, Earth, PresentationChart, ChartPie, ChartBubble, Cog, Database, Cloud, Globe, Server, Lightbulb, Crown, SealCheck, Briefcase, Rocket, Shield, CheckCircle, Lightning, Heart, Collapse, Bold, ArrowUp, ArrowDown, Duplicate, Lock, EyeStriked, Eye } from "@strapi/icons";
6
+ import { ChevronRight, Folder, Magic, Pencil, Upload, Sparkle, Plus, Cross, Download, ArrowClockwise, House, ChevronDown, FilePdf, File as File$1, More, ExternalLink, Trash, User, Star, SquaresFour, Quotes, PriceTag, Paragraph, NumberList, Minus, Italic, Image as Image$1, HeadingTwo, HeadingOne, Hashtag, Feather, Drag, BulletList, ArrowLeft, Check, ArrowsCounterClockwise, ArrowLineLeft, ArrowLineRight, Expand, WarningCircle, Play, Phone, Mail, PinMap, Earth, PresentationChart, ChartPie, ChartBubble, Cog, Database, Cloud, Globe, Server, Lightbulb, Crown, SealCheck, Briefcase, Rocket, Shield, CheckCircle, Lightning, Heart, Collapse, Bold, ArrowUp, ArrowDown, Duplicate, Lock, EyeStriked, Eye } from "@strapi/icons";
7
7
  import { useTheme } from "styled-components";
8
8
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
9
9
  import { toPng, toSvg, toJpeg } from "html-to-image";
@@ -1171,7 +1171,7 @@ const closestRatio = (width, height, ratios) => {
1171
1171
  };
1172
1172
  return ratios.reduce((best, r) => Math.abs(value(r) - target) < Math.abs(value(best) - target) ? r : best);
1173
1173
  };
1174
- const BASE = "/strapi-content-generator-gemini/image-studio";
1174
+ const BASE$2 = "/strapi-content-generator-gemini/image-studio";
1175
1175
  const toResult = ({ image, mimeType, model, grounding }) => ({
1176
1176
  src: `data:${mimeType};base64,${image}`,
1177
1177
  model,
@@ -1183,7 +1183,7 @@ const errorMessage = (err, fallback) => {
1183
1183
  };
1184
1184
  const createGeneratorApi = (client) => ({
1185
1185
  async models() {
1186
- const { data } = await client.get(`${BASE}/models`);
1186
+ const { data } = await client.get(`${BASE$2}/models`);
1187
1187
  return data;
1188
1188
  },
1189
1189
  async generate(p) {
@@ -1196,7 +1196,7 @@ const createGeneratorApi = (client) => ({
1196
1196
  form.append("allowPeople", String(p.allowPeople));
1197
1197
  form.append("grounding", String(p.grounding));
1198
1198
  p.references.forEach((f) => form.append("references", f, f.name));
1199
- const { data } = await client.post(`${BASE}/generate`, form);
1199
+ const { data } = await client.post(`${BASE$2}/generate`, form);
1200
1200
  return toResult(data);
1201
1201
  },
1202
1202
  async edit(p) {
@@ -1209,11 +1209,11 @@ const createGeneratorApi = (client) => ({
1209
1209
  p.references.forEach((f) => form.append("references", f, f.name));
1210
1210
  if (p.aspectRatio) form.append("aspectRatio", p.aspectRatio);
1211
1211
  form.append("image", new File([blob], `base.${blob.type.split("/")[1] ?? "png"}`, { type: blob.type }));
1212
- const { data } = await client.post(`${BASE}/edit`, form);
1212
+ const { data } = await client.post(`${BASE$2}/edit`, form);
1213
1213
  return toResult(data);
1214
1214
  },
1215
1215
  async expand(prompt) {
1216
- const { data } = await client.post(`${BASE}/expand`, { prompt });
1216
+ const { data } = await client.post(`${BASE$2}/expand`, { prompt });
1217
1217
  return data.prompt;
1218
1218
  }
1219
1219
  });
@@ -1835,7 +1835,7 @@ const isImage = (f) => (f.mime ?? "").startsWith("image/");
1835
1835
  const canRemoveBackground = (f) => isImage(f) && f.mime !== "image/svg+xml";
1836
1836
  const isGenerated = (f) => (f.caption ?? "").startsWith("AI generated content");
1837
1837
  const formatSize = (kb = 0) => kb >= 1024 ? `${(kb / 1024).toFixed(1)} MB` : `${Math.round(kb)} KB`;
1838
- const FileCard = ({ file, onAction, busy }) => {
1838
+ const FileCard = ({ file, onAction, busy, drive }) => {
1839
1839
  const c = useThemeColors();
1840
1840
  const thumb2 = file.formats?.thumbnail?.url ?? file.formats?.small?.url ?? file.url;
1841
1841
  return /* @__PURE__ */ jsxs(
@@ -1874,7 +1874,8 @@ const FileCard = ({ file, onAction, busy }) => {
1874
1874
  /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1875
1875
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatSize(file.size) }),
1876
1876
  isGenerated(file) && /* @__PURE__ */ jsx(Badge, { children: "Generada" }),
1877
- busy && /* @__PURE__ */ jsx(Badge, { active: true, children: busy })
1877
+ busy && /* @__PURE__ */ jsx(Badge, { active: true, children: busy }),
1878
+ drive?.label && /* @__PURE__ */ jsx(Badge, { variant: drive.tone, title: drive.title, children: drive.label })
1878
1879
  ] })
1879
1880
  ] }),
1880
1881
  /* @__PURE__ */ jsxs(Menu.Root, { children: [
@@ -1886,6 +1887,8 @@ const FileCard = ({ file, onAction, busy }) => {
1886
1887
  /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("replace", file), children: "Reemplazar" }),
1887
1888
  /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("move", file), children: "Mover a…" }),
1888
1889
  canRemoveBackground(file) && /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("removeBackground", file), disabled: !!busy, children: "Quitar fondo" }),
1890
+ drive?.action === "mark" && /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("syncDrive", file), children: "Sincronizar con Drive" }),
1891
+ drive?.action === "unmark" && /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("unsyncDrive", file), children: "Dejar de sincronizar" }),
1889
1892
  /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("delete", file), variant: "danger", children: "Eliminar" })
1890
1893
  ] })
1891
1894
  ] })
@@ -1894,10 +1897,260 @@ const FileCard = ({ file, onAction, busy }) => {
1894
1897
  }
1895
1898
  );
1896
1899
  };
1900
+ const BASE$1 = "/strapi-content-generator-gemini/drive";
1901
+ const FOLDER = "application/vnd.google-apps.folder";
1902
+ const NATIVE_TYPES = {
1903
+ "application/vnd.google-apps.document": "Documento de Google",
1904
+ "application/vnd.google-apps.spreadsheet": "Hoja de cálculo de Google",
1905
+ "application/vnd.google-apps.presentation": "Presentación de Google",
1906
+ "application/vnd.google-apps.form": "Formulario de Google",
1907
+ "application/vnd.google-apps.drawing": "Dibujo de Google",
1908
+ "application/vnd.google-apps.shortcut": "Acceso directo"
1909
+ };
1910
+ const typeLabel = (mime) => {
1911
+ if (mime === FOLDER) return "Carpeta";
1912
+ if (NATIVE_TYPES[mime]) return NATIVE_TYPES[mime];
1913
+ if (mime.startsWith("application/vnd.google-apps.")) return "Archivo de Google";
1914
+ if (mime === "application/pdf") return "PDF";
1915
+ if (mime.startsWith("image/")) return "Imagen";
1916
+ if (mime.startsWith("video/")) return "Video";
1917
+ if (mime.startsWith("audio/")) return "Audio";
1918
+ return "Archivo";
1919
+ };
1920
+ const TypeIcon = ({ mime }) => mime === FOLDER ? /* @__PURE__ */ jsx(Folder, { width: 16, height: 16, "aria-hidden": true }) : mime === "application/pdf" ? /* @__PURE__ */ jsx(FilePdf, { width: 16, height: 16, "aria-hidden": true }) : /* @__PURE__ */ jsx(File$1, { width: 16, height: 16, "aria-hidden": true });
1921
+ const formatDate = (iso) => iso ? new Date(iso).toLocaleString("es-MX", { dateStyle: "medium", timeStyle: "short", timeZone: "America/Mexico_City" }) : "—";
1922
+ const confirmText = (item, count) => {
1923
+ const parts = [`«${item.name}» irá a la papelera de Drive, donde se puede recuperar durante 30 días.`];
1924
+ if (item.mimeType === FOLDER && count) {
1925
+ const total = count.files + count.folders;
1926
+ parts.push(
1927
+ total === 0 ? "La carpeta está vacía." : `Contiene ${count.truncated ? "más de " : ""}${total.toLocaleString("es-MX")} ${total === 1 ? "elemento" : "elementos"} (${count.files} archivos y ${count.folders} carpetas), que también se van.`
1928
+ );
1929
+ }
1930
+ if (item.synced || count?.synced) parts.push("Está sincronizado: también se borrará de la Biblioteca del CMS.");
1931
+ return parts.join(" ");
1932
+ };
1933
+ const serverMessage$1 = (error, fallback) => error?.response?.data?.error?.message ?? (error instanceof Error && error.message ? error.message : fallback);
1934
+ const useDriveBrowserApi = () => {
1935
+ const { get, del } = useFetchClient();
1936
+ return useMemo(
1937
+ () => ({
1938
+ browse: async (folderId, pageToken) => {
1939
+ try {
1940
+ return (await get(`${BASE$1}/folders/${encodeURIComponent(folderId)}`, { params: pageToken ? { pageToken } : {} })).data;
1941
+ } catch (error) {
1942
+ throw new Error(serverMessage$1(error, "No se pudo leer Google Drive."));
1943
+ }
1944
+ },
1945
+ count: async (id) => (await get(`${BASE$1}/items/${encodeURIComponent(id)}/count`)).data,
1946
+ trash: async (id) => {
1947
+ try {
1948
+ await del(`${BASE$1}/items/${encodeURIComponent(id)}`);
1949
+ } catch (error) {
1950
+ throw new Error(serverMessage$1(error, "No se pudo mandar a la papelera."));
1951
+ }
1952
+ }
1953
+ }),
1954
+ [get, del]
1955
+ );
1956
+ };
1957
+ const DriveTab = ({ api: injected, notify }) => {
1958
+ const fallbackApi = useDriveBrowserApi();
1959
+ const api = injected ?? fallbackApi;
1960
+ const { toggleNotification } = useNotification();
1961
+ const say = notify ?? ((type, message) => toggleNotification({ type, message }));
1962
+ const [folderId, setFolderId] = useState("root");
1963
+ const [page, setPage] = useState(null);
1964
+ const [error, setError] = useState(null);
1965
+ const [loading2, setLoading] = useState(true);
1966
+ const [loadingMore, setLoadingMore] = useState(false);
1967
+ const [doomed, setDoomed] = useState(null);
1968
+ const load = useCallback(async () => {
1969
+ setLoading(true);
1970
+ setError(null);
1971
+ try {
1972
+ setPage(await api.browse(folderId));
1973
+ } catch (err) {
1974
+ setError(err instanceof Error ? err.message : "No se pudo leer Google Drive.");
1975
+ } finally {
1976
+ setLoading(false);
1977
+ }
1978
+ }, [api, folderId]);
1979
+ useEffect(() => {
1980
+ load();
1981
+ }, [load]);
1982
+ const more = async () => {
1983
+ if (!page?.nextPageToken) return;
1984
+ setLoadingMore(true);
1985
+ try {
1986
+ const next = await api.browse(folderId, page.nextPageToken);
1987
+ setPage({ ...next, items: [...page.items, ...next.items] });
1988
+ } finally {
1989
+ setLoadingMore(false);
1990
+ }
1991
+ };
1992
+ const askDelete = async (item) => {
1993
+ setDoomed({ item, count: null });
1994
+ if (item.mimeType === FOLDER) {
1995
+ try {
1996
+ const count = await api.count(item.id);
1997
+ setDoomed((d) => d?.item.id === item.id ? { item, count } : d);
1998
+ } catch {
1999
+ }
2000
+ }
2001
+ };
2002
+ const confirmDelete = async () => {
2003
+ if (!doomed) return;
2004
+ const { item } = doomed;
2005
+ setDoomed(null);
2006
+ try {
2007
+ await api.trash(item.id);
2008
+ say("success", `«${item.name}» se mandó a la papelera de Drive.`);
2009
+ await load();
2010
+ } catch (err) {
2011
+ say("danger", err instanceof Error ? err.message : "No se pudo mandar a la papelera.");
2012
+ }
2013
+ };
2014
+ if (error) return /* @__PURE__ */ jsx(EmptyNote, { children: error });
2015
+ return /* @__PURE__ */ jsxs(Box, { children: [
2016
+ /* @__PURE__ */ jsxs(Flex, { "data-testid": "drive-breadcrumb", gap: 1, alignItems: "center", wrap: "wrap", paddingBottom: 4, children: [
2017
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", onClick: () => setFolderId("root"), children: "Mi unidad" }),
2018
+ (page?.path ?? []).map((p, i, all) => /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
2019
+ /* @__PURE__ */ jsx(ChevronRight, { width: 10, height: 10, "aria-hidden": true }),
2020
+ i === all.length - 1 ? /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: p.name }) : /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", onClick: () => setFolderId(p.id), children: p.name })
2021
+ ] }, p.id))
2022
+ ] }),
2023
+ loading2 ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Cargando Google Drive…" }) }) : !page?.items.length ? /* @__PURE__ */ jsx(EmptyNote, { children: "Esta carpeta de Drive está vacía." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2024
+ /* @__PURE__ */ jsxs(Table, { colCount: 6, rowCount: page.items.length + 1, children: [
2025
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
2026
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Nombre" }) }),
2027
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Tipo" }) }),
2028
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Modificado" }) }),
2029
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Tamaño" }) }),
2030
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Estado" }) }),
2031
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(VisuallyHidden, { children: "Acciones" }) })
2032
+ ] }) }),
2033
+ /* @__PURE__ */ jsx(Tbody, { children: page.items.map((item) => /* @__PURE__ */ jsxs(Tr, { children: [
2034
+ /* @__PURE__ */ jsx(Td, { style: { maxWidth: 420, whiteSpace: "normal" }, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
2035
+ /* @__PURE__ */ jsx(TypeIcon, { mime: item.mimeType }),
2036
+ item.mimeType === FOLDER ? /* @__PURE__ */ jsx(TextButton, { onClick: () => setFolderId(item.id), style: { textAlign: "left", height: "auto" }, children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", textColor: "neutral800", style: { overflowWrap: "anywhere" }, children: item.name }) }) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", style: { overflowWrap: "anywhere" }, children: item.name })
2037
+ ] }) }),
2038
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", children: typeLabel(item.mimeType) }) }),
2039
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", style: { whiteSpace: "nowrap" }, children: formatDate(item.modifiedTime) }) }),
2040
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", children: item.size === null ? "—" : formatSize(item.size / 1e3) }) }),
2041
+ /* @__PURE__ */ jsx(Td, { children: item.synced && /* @__PURE__ */ jsx(Badge, { variant: "success", children: "Sincronizado" }) }),
2042
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "end", children: [
2043
+ item.webViewLink && /* @__PURE__ */ jsx(
2044
+ LinkButton,
2045
+ {
2046
+ variant: "tertiary",
2047
+ size: "S",
2048
+ href: item.webViewLink,
2049
+ target: "_blank",
2050
+ rel: "noreferrer noopener",
2051
+ endIcon: /* @__PURE__ */ jsx(ExternalLink, {}),
2052
+ children: "Abrir en Drive"
2053
+ }
2054
+ ),
2055
+ /* @__PURE__ */ jsx(IconButton, { label: `Eliminar ${item.name}`, variant: "ghost", onClick: () => askDelete(item), children: /* @__PURE__ */ jsx(Trash, {}) })
2056
+ ] }) })
2057
+ ] }, item.id)) })
2058
+ ] }),
2059
+ page.nextPageToken && /* @__PURE__ */ jsx(Flex, { justifyContent: "center", paddingTop: 4, children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: more, loading: loadingMore, children: "Mostrar más" }) })
2060
+ ] }),
2061
+ /* @__PURE__ */ jsx(
2062
+ ConfirmDialog,
2063
+ {
2064
+ open: !!doomed,
2065
+ title: "¿Mandar a la papelera de Drive?",
2066
+ confirmLabel: "Mandar a la papelera",
2067
+ onClose: () => setDoomed(null),
2068
+ onConfirm: confirmDelete,
2069
+ children: doomed ? confirmText(doomed.item, doomed.count) : ""
2070
+ }
2071
+ )
2072
+ ] });
2073
+ };
2074
+ const BASE = "/strapi-content-generator-gemini/drive";
2075
+ const POLL_MS = 1e4;
2076
+ const LABELS = {
2077
+ synced: { label: "Drive", tone: "success" },
2078
+ pending: { label: "Drive: pendiente", tone: "neutral" },
2079
+ error: { label: "Drive: error", tone: "danger" },
2080
+ skipped: { label: "Drive: omitido", tone: "warning" }
2081
+ };
2082
+ const driveView = (links, key, parentInside) => {
2083
+ if (!links?.connected) return null;
2084
+ const item = links.items[key];
2085
+ if (item?.inside) {
2086
+ return { ...LABELS[item.status], title: item.error ?? void 0, action: item.root ? "unmark" : null };
2087
+ }
2088
+ if (parentInside) return { ...LABELS.pending, action: null };
2089
+ return { label: null, tone: "neutral", action: "mark" };
2090
+ };
2091
+ const serverMessage = (error, fallback) => error?.response?.data?.error?.message ?? fallback;
2092
+ const useDriveSync = (watching = false) => {
2093
+ const { get, post, del } = useFetchClient();
2094
+ const [links, setLinks] = useState(null);
2095
+ const refresh = useCallback(async () => {
2096
+ try {
2097
+ const { data } = await get(`${BASE}/links`);
2098
+ setLinks(data);
2099
+ } catch {
2100
+ setLinks(null);
2101
+ }
2102
+ }, [get]);
2103
+ useEffect(() => {
2104
+ refresh();
2105
+ }, [refresh]);
2106
+ const busy = !!links?.connected && (watching || links.pending > 0 || Object.values(links.items).some((i) => i.status === "pending"));
2107
+ useEffect(() => {
2108
+ if (!busy) return;
2109
+ const timer = setInterval(refresh, POLL_MS);
2110
+ return () => clearInterval(timer);
2111
+ }, [busy, refresh]);
2112
+ useEffect(() => {
2113
+ const onVisible = () => document.visibilityState === "visible" && refresh();
2114
+ document.addEventListener("visibilitychange", onVisible);
2115
+ return () => document.removeEventListener("visibilitychange", onVisible);
2116
+ }, [refresh]);
2117
+ return useMemo(
2118
+ () => ({
2119
+ links,
2120
+ refresh,
2121
+ async mark(cmsType, cmsId) {
2122
+ try {
2123
+ await post(`${BASE}/links`, { cmsType, cmsId });
2124
+ } catch (error) {
2125
+ throw new Error(serverMessage(error, "No se pudo sincronizar con Drive."));
2126
+ } finally {
2127
+ await refresh();
2128
+ }
2129
+ },
2130
+ async unmark(cmsType, cmsId) {
2131
+ try {
2132
+ await del(`${BASE}/links/${cmsType}/${cmsId}`);
2133
+ } catch (error) {
2134
+ throw new Error(serverMessage(error, "No se pudo dejar de sincronizar."));
2135
+ } finally {
2136
+ await refresh();
2137
+ }
2138
+ }
2139
+ }),
2140
+ [links, refresh, post, del]
2141
+ );
2142
+ };
2143
+ const uploadSummary = (total, failed) => {
2144
+ if (failed.length === 0) return { type: "success", message: total === 1 ? "Archivo subido" : `${total} archivos subidos` };
2145
+ if (failed.length === total) return { type: "danger", message: `No se pudo subir: ${failed.join(", ")}.` };
2146
+ return { type: "warning", message: `${total - failed.length} de ${total} archivos subidos. No se pudo subir: ${failed.join(", ")}.` };
2147
+ };
1897
2148
  const status = (err) => err?.response?.status ?? err?.status;
1898
2149
  const LibraryTab = () => {
1899
2150
  const c = useThemeColors();
1900
2151
  const { api, notify } = useLibraryApi();
2152
+ const [drivePending, setDrivePending] = useState(false);
2153
+ const drive = useDriveSync(drivePending);
1901
2154
  const [tree, setTree] = useState([]);
1902
2155
  const [counts, setCounts] = useState({});
1903
2156
  const [current, setCurrent] = useState(null);
@@ -1908,7 +2161,10 @@ const LibraryTab = () => {
1908
2161
  const [dragOver, setDragOver] = useState(false);
1909
2162
  const [bgBusy, setBgBusy] = useState({});
1910
2163
  const [bgTarget, setBgTarget] = useState(null);
2164
+ const [uploading, setUploading] = useState(null);
1911
2165
  const uploadInput = useRef(null);
2166
+ const currentRef = useRef(current);
2167
+ currentRef.current = current;
1912
2168
  const replaceInput = useRef(null);
1913
2169
  const replaceTarget = useRef(null);
1914
2170
  const path = pathTo(tree, current);
@@ -1941,7 +2197,20 @@ const LibraryTab = () => {
1941
2197
  useEffect(() => {
1942
2198
  loadFiles();
1943
2199
  }, [loadFiles]);
1944
- const refresh = () => Promise.all([loadTree(), loadFiles()]);
2200
+ const refresh = () => Promise.all([loadTree(), loadFiles(), drive.refresh()]);
2201
+ const toggleDrive = async (mode, cmsType, cmsId) => {
2202
+ try {
2203
+ if (mode === "mark") {
2204
+ await drive.mark(cmsType, cmsId);
2205
+ notify("success", "Se sincronizará con Drive en unos segundos.");
2206
+ } else {
2207
+ await drive.unmark(cmsType, cmsId);
2208
+ notify("success", "Dejó de sincronizarse. No se borró nada en Drive ni aquí.");
2209
+ }
2210
+ } catch (err) {
2211
+ notify("danger", err instanceof Error ? err.message : "No se pudo completar la operación con Drive.");
2212
+ }
2213
+ };
1945
2214
  const run = async (action, ok) => {
1946
2215
  try {
1947
2216
  await action();
@@ -1953,9 +2222,30 @@ const LibraryTab = () => {
1953
2222
  await refresh();
1954
2223
  }
1955
2224
  };
1956
- const upload = (list) => {
2225
+ useEffect(() => {
2226
+ if (!uploading) return;
2227
+ const warn = (e) => e.preventDefault();
2228
+ window.addEventListener("beforeunload", warn);
2229
+ return () => window.removeEventListener("beforeunload", warn);
2230
+ }, [uploading]);
2231
+ const upload = async (list) => {
1957
2232
  const arr = [...list ?? []];
1958
- if (arr.length) run(() => api.uploadFiles(arr, current), arr.length === 1 ? "Archivo subido" : `${arr.length} archivos subidos`);
2233
+ if (!arr.length || uploading) return;
2234
+ const folderId = current;
2235
+ const failed = [];
2236
+ for (const [i, file] of arr.entries()) {
2237
+ setUploading({ done: i, total: arr.length, name: file.name });
2238
+ try {
2239
+ const uploaded = await api.uploadFiles([file], folderId);
2240
+ if (currentRef.current === folderId) setFiles((prev) => [...uploaded, ...prev]);
2241
+ } catch {
2242
+ failed.push(file.name);
2243
+ }
2244
+ }
2245
+ setUploading(null);
2246
+ const { type, message } = uploadSummary(arr.length, failed);
2247
+ notify(type, message);
2248
+ await refresh();
1959
2249
  };
1960
2250
  const setBgStatus = (file, s) => setBgBusy((b) => {
1961
2251
  const next = { ...b };
@@ -1998,12 +2288,24 @@ const LibraryTab = () => {
1998
2288
  if (action === "rename") return setDialog({ kind: "renameFile", file });
1999
2289
  if (action === "move") return setDialog({ kind: "moveFile", file });
2000
2290
  if (action === "delete") return setDialog({ kind: "deleteFile", file });
2291
+ if (action === "syncDrive") return toggleDrive("mark", "file", file.id);
2292
+ if (action === "unsyncDrive") return toggleDrive("unmark", "file", file.id);
2001
2293
  };
2002
2294
  const validateFolder = (parent, except) => (name) => cleanFolderName(name) !== except && nameTaken(tree, parent, name) ? "Ya existe una carpeta con ese nombre aquí." : null;
2295
+ const pendingShown = (() => {
2296
+ const inside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2297
+ const isPending = (key) => driveView(drive.links, key, inside)?.label === "Drive: pendiente";
2298
+ return files.some((f) => isPending(`file:${f.id}`)) || childrenOf(tree, current).some((f) => isPending(`folder:${f.id}`));
2299
+ })();
2300
+ useEffect(() => {
2301
+ setDrivePending(pendingShown);
2302
+ }, [pendingShown]);
2003
2303
  if (forbidden) {
2004
2304
  return /* @__PURE__ */ jsx(EmptyNote, { children: "Para usar la biblioteca necesitas permiso de la Media Library de Strapi. Pídelo a un administrador." });
2005
2305
  }
2006
2306
  const subfolders = childrenOf(tree, current);
2307
+ const currentInside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2308
+ const folderDrive = current !== null ? driveView(drive.links, `folder:${current}`, !!(folder && path.length > 1 && drive.links?.items[`folder:${path[path.length - 2].id}`]?.inside)) : null;
2007
2309
  return /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "stretch", children: [
2008
2310
  /* @__PURE__ */ jsx(Box, { width: "280px", shrink: 0, background: "neutral0", padding: 3, hasRadius: true, style: { maxHeight: "calc(100vh - 220px)", overflowY: "auto" }, children: /* @__PURE__ */ jsx(
2009
2311
  FolderTree,
@@ -2025,7 +2327,7 @@ const LibraryTab = () => {
2025
2327
  hasRadius: true,
2026
2328
  position: "relative",
2027
2329
  onDragOver: (e) => {
2028
- if (e.dataTransfer.types.includes("Files")) {
2330
+ if (e.dataTransfer.types.includes("Files") && !uploading) {
2029
2331
  e.preventDefault();
2030
2332
  setDragOver(true);
2031
2333
  }
@@ -2037,7 +2339,7 @@ const LibraryTab = () => {
2037
2339
  if (e.dataTransfer.files.length) {
2038
2340
  e.preventDefault();
2039
2341
  setDragOver(false);
2040
- upload(e.dataTransfer.files);
2342
+ if (!uploading) upload(e.dataTransfer.files);
2041
2343
  }
2042
2344
  },
2043
2345
  children: [
@@ -2050,12 +2352,15 @@ const LibraryTab = () => {
2050
2352
  ] }, f.id))
2051
2353
  ] }),
2052
2354
  /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
2355
+ folder && folderDrive?.label && /* @__PURE__ */ jsx(Badge, { variant: folderDrive.tone, title: folderDrive.title, children: folderDrive.label }),
2356
+ folder && folderDrive?.action === "mark" && /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => toggleDrive("mark", "folder", folder.id), children: "Sincronizar con Drive" }),
2357
+ folder && folderDrive?.action === "unmark" && /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: () => toggleDrive("unmark", "folder", folder.id), children: "Dejar de sincronizar" }),
2053
2358
  folder && /* @__PURE__ */ jsxs(Fragment, { children: [
2054
2359
  /* @__PURE__ */ jsx(Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsx(Pencil, {}), onClick: () => setDialog({ kind: "renameFolder" }), children: "Renombrar carpeta" }),
2055
2360
  /* @__PURE__ */ jsx(Button, { variant: "danger-light", startIcon: /* @__PURE__ */ jsx(Trash, {}), onClick: () => setDialog({ kind: "deleteFolder" }), children: "Eliminar carpeta" })
2056
2361
  ] }),
2057
2362
  /* @__PURE__ */ jsx(Button, { variant: "secondary", startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: () => setDialog({ kind: "newFolder" }), children: "Nueva carpeta" }),
2058
- /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Upload, {}), onClick: () => uploadInput.current?.click(), children: "Subir archivos" })
2363
+ /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Upload, {}), onClick: () => uploadInput.current?.click(), disabled: !!uploading, loading: !!uploading, children: "Subir archivos" })
2059
2364
  ] })
2060
2365
  ] }),
2061
2366
  subfolders.length > 0 && /* @__PURE__ */ jsx(Grid.Root, { gap: 2, paddingBottom: 4, children: subfolders.map((f) => /* @__PURE__ */ jsx(Grid.Item, { col: 3, s: 6, xs: 12, children: /* @__PURE__ */ jsx(
@@ -2074,11 +2379,26 @@ const LibraryTab = () => {
2074
2379
  children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
2075
2380
  /* @__PURE__ */ jsx(Folder, { width: 16, height: 16, "aria-hidden": true }),
2076
2381
  /* @__PURE__ */ jsx(Typography, { ellipsis: true, style: { flex: 1 }, children: f.name }),
2382
+ (() => {
2383
+ const view = driveView(drive.links, `folder:${f.id}`, currentInside);
2384
+ return view?.label ? /* @__PURE__ */ jsx(Badge, { size: "S", variant: view.tone, title: view.title, children: view.label }) : null;
2385
+ })(),
2077
2386
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: counts[f.id]?.files ?? 0 })
2078
2387
  ] })
2079
2388
  }
2080
2389
  ) }, f.id)) }),
2081
- loading2 ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Cargando archivos…" }) }) : files.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { children: subfolders.length ? "No hay archivos sueltos en esta carpeta." : "Carpeta vacía. Arrastra archivos aquí o usa «Subir archivos»." }) : /* @__PURE__ */ jsx(Grid.Root, { gap: 4, children: files.map((f) => /* @__PURE__ */ jsx(Grid.Item, { col: 3, m: 4, s: 6, xs: 12, children: /* @__PURE__ */ jsx(FileCard, { file: f, onAction: onFileAction, busy: bgBusy[f.id] }) }, f.id)) }),
2390
+ uploading && /* @__PURE__ */ jsx(Box, { padding: 3, marginBottom: 4, hasRadius: true, background: "primary100", borderColor: "primary200", role: "status", "aria-live": "polite", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
2391
+ /* @__PURE__ */ jsxs(Typography, { textColor: "primary700", children: [
2392
+ "Subiendo ",
2393
+ uploading.done + 1,
2394
+ " de ",
2395
+ uploading.total,
2396
+ " · ",
2397
+ uploading.name
2398
+ ] }),
2399
+ /* @__PURE__ */ jsx(ProgressBar, { value: Math.round(uploading.done / uploading.total * 100), children: `${uploading.done} de ${uploading.total} archivos subidos` })
2400
+ ] }) }),
2401
+ loading2 ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Cargando archivos…" }) }) : files.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { children: subfolders.length ? "No hay archivos sueltos en esta carpeta." : "Carpeta vacía. Arrastra archivos aquí o usa «Subir archivos»." }) : /* @__PURE__ */ jsx(Grid.Root, { gap: 4, children: files.map((f) => /* @__PURE__ */ jsx(Grid.Item, { col: 3, m: 4, s: 6, xs: 12, children: /* @__PURE__ */ jsx(FileCard, { file: f, onAction: onFileAction, busy: bgBusy[f.id], drive: driveView(drive.links, `file:${f.id}`, currentInside) }) }, f.id)) }),
2082
2402
  dragOver && /* @__PURE__ */ jsx(Flex, { position: "absolute", justifyContent: "center", background: "primary100", hasRadius: true, style: { inset: 0, border: `2px dashed ${c.primary600}`, opacity: 0.95 }, children: /* @__PURE__ */ jsxs(Typography, { variant: "beta", textColor: "primary700", children: [
2083
2403
  "Suelta para subir a «",
2084
2404
  folder?.name ?? ROOT_LABEL,
@@ -2760,6 +3080,15 @@ const editorReducer = (state, action) => {
2760
3080
  }
2761
3081
  }
2762
3082
  };
3083
+ const ORIGIN_URL = "/strapi-content-generator-gemini/drive/origin";
3084
+ const recordTemplateOrigin = async (post, files, template) => {
3085
+ for (const file of files) {
3086
+ try {
3087
+ await post(ORIGIN_URL, { fileId: file.id, templateDocumentId: template.documentId ?? template.id, templateName: template.name });
3088
+ } catch {
3089
+ }
3090
+ }
3091
+ };
2763
3092
  const useEditorIO = () => {
2764
3093
  const { post } = useFetchClient();
2765
3094
  const { toggleNotification } = useNotification();
@@ -2792,7 +3121,8 @@ const useEditorIO = () => {
2792
3121
  if (!node) return false;
2793
3122
  try {
2794
3123
  const blob = await renderPiece(node, template, "png");
2795
- await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3124
+ const saved = await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3125
+ await recordTemplateOrigin((u, body) => post(u, body), saved, template);
2796
3126
  toggleNotification({ type: "success", message: "Pieza guardada en la biblioteca." });
2797
3127
  return true;
2798
3128
  } catch {
@@ -2800,7 +3130,7 @@ const useEditorIO = () => {
2800
3130
  return false;
2801
3131
  }
2802
3132
  },
2803
- [api, toggleNotification]
3133
+ [api, post, toggleNotification]
2804
3134
  );
2805
3135
  return { loadImage: loadImage2, exportAs, saveToLibrary };
2806
3136
  };
@@ -5245,7 +5575,8 @@ const TemplatesTab = () => {
5245
5575
  const STUDIO_TABS = [
5246
5576
  { value: "generator", label: "Generador" },
5247
5577
  { value: "templates", label: "Plantillas" },
5248
- { value: "library", label: "Imágenes y documentos" }
5578
+ { value: "library", label: "Imágenes y documentos" },
5579
+ { value: "drive", label: "Google Drive" }
5249
5580
  ];
5250
5581
  const PANEL = { height: "100%", overflowY: "auto" };
5251
5582
  const isStudioTab = (v) => STUDIO_TABS.some((t) => t.value === v);
@@ -5273,7 +5604,8 @@ const ImageStudioTabs = () => {
5273
5604
  /* @__PURE__ */ jsxs(Box, { flex: "1", style: { minHeight: 0 }, children: [
5274
5605
  /* @__PURE__ */ jsx(Tabs.Content, { value: "generator", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(GeneratorTab, {}) }) }),
5275
5606
  /* @__PURE__ */ jsx(Tabs.Content, { value: "templates", style: PANEL, children: /* @__PURE__ */ jsx(TemplatesTab, {}) }),
5276
- /* @__PURE__ */ jsx(Tabs.Content, { value: "library", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(LibraryTab, {}) }) })
5607
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "library", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(LibraryTab, {}) }) }),
5608
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "drive", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: tab === "drive" && /* @__PURE__ */ jsx(DriveTab, {}) }) })
5277
5609
  ] })
5278
5610
  ]
5279
5611
  }