@interware/content-generator 0.7.2 → 0.8.0

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 (45) hide show
  1. package/dist/_chunks/{App-CTh6owSM.mjs → App-DD1MzaWm.mjs} +2 -2
  2. package/dist/_chunks/{App-ugsCmlsC.js → App-DspaMfyW.js} +2 -2
  3. package/dist/_chunks/{ImageStudioTabs-Umf3GODW.mjs → ImageStudioTabs-DwaLxRjl.mjs} +308 -16
  4. package/dist/_chunks/{ImageStudioTabs-BFEF9ggK.js → ImageStudioTabs-uXSBkFeu.js} +306 -14
  5. package/dist/_chunks/{Settings-CdGW2O8h.mjs → Settings-C6TbUrgU.mjs} +170 -2
  6. package/dist/_chunks/{Settings-Sk78JUBS.js → Settings-ps0o7-fC.js} +169 -1
  7. package/dist/_chunks/{StudioApp-CrGn-9Gg.mjs → StudioApp-B5I1OfGw.mjs} +2 -2
  8. package/dist/_chunks/{StudioApp-DoC0z4eh.js → StudioApp-B7kGpqqo.js} +2 -2
  9. package/dist/_chunks/{index-C2s9Jhcc.js → index-BudahdfX.js} +3 -3
  10. package/dist/_chunks/{index-D6UAvh53.mjs → index-zenHkX-p.mjs} +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/driveSync.d.ts +41 -0
  18. package/dist/admin/src/features/templates/export/templateOrigin.d.ts +10 -0
  19. package/dist/admin/src/lib/use-cases/drive/index.d.ts +3 -0
  20. package/dist/server/index.js +1411 -49
  21. package/dist/server/index.mjs +1409 -47
  22. package/dist/server/src/bootstrap.d.ts +2 -0
  23. package/dist/server/src/content-types/drive-link/index.d.ts +62 -0
  24. package/dist/server/src/content-types/drive-link/schema.d.ts +68 -0
  25. package/dist/server/src/content-types/drive-op/index.d.ts +46 -0
  26. package/dist/server/src/content-types/drive-op/schema.d.ts +49 -0
  27. package/dist/server/src/content-types/index.d.ts +106 -0
  28. package/dist/server/src/controllers/drive.d.ts +30 -0
  29. package/dist/server/src/controllers/index.d.ts +130 -0
  30. package/dist/server/src/drive/client.d.ts +32 -0
  31. package/dist/server/src/drive/clientConfig.d.ts +27 -0
  32. package/dist/server/src/drive/common.d.ts +6 -0
  33. package/dist/server/src/drive/fakes.d.ts +104 -0
  34. package/dist/server/src/drive/files.d.ts +136 -0
  35. package/dist/server/src/drive/lifecycles.d.ts +7 -0
  36. package/dist/server/src/drive/links.d.ts +23 -0
  37. package/dist/server/src/drive/model.d.ts +99 -0
  38. package/dist/server/src/drive/oauth.d.ts +47 -0
  39. package/dist/server/src/drive/pull.d.ts +23 -0
  40. package/dist/server/src/drive/strapiPorts.d.ts +10 -0
  41. package/dist/server/src/drive/sync.d.ts +40 -0
  42. package/dist/server/src/index.d.ts +294 -0
  43. package/dist/server/src/services/drive.d.ts +141 -0
  44. package/dist/server/src/services/index.d.ts +55 -0
  45. 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-zenHkX-p.mjs";
9
+ import { L as LibraryImagePicker, P as PromptComposer, a as LEGACY_ROUTES, S as StudioRedirect, l as legacyRedirect } from "./ImageStudioTabs-DwaLxRjl.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-BudahdfX.js");
11
+ const ImageStudioTabs = require("./ImageStudioTabs-uXSBkFeu.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, 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-zenHkX-p.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,255 @@ 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
+ };
1897
2143
  const status = (err) => err?.response?.status ?? err?.status;
1898
2144
  const LibraryTab = () => {
1899
2145
  const c = useThemeColors();
1900
2146
  const { api, notify } = useLibraryApi();
2147
+ const [drivePending, setDrivePending] = useState(false);
2148
+ const drive = useDriveSync(drivePending);
1901
2149
  const [tree, setTree] = useState([]);
1902
2150
  const [counts, setCounts] = useState({});
1903
2151
  const [current, setCurrent] = useState(null);
@@ -1941,7 +2189,20 @@ const LibraryTab = () => {
1941
2189
  useEffect(() => {
1942
2190
  loadFiles();
1943
2191
  }, [loadFiles]);
1944
- const refresh = () => Promise.all([loadTree(), loadFiles()]);
2192
+ const refresh = () => Promise.all([loadTree(), loadFiles(), drive.refresh()]);
2193
+ const toggleDrive = async (mode, cmsType, cmsId) => {
2194
+ try {
2195
+ if (mode === "mark") {
2196
+ await drive.mark(cmsType, cmsId);
2197
+ notify("success", "Se sincronizará con Drive en unos segundos.");
2198
+ } else {
2199
+ await drive.unmark(cmsType, cmsId);
2200
+ notify("success", "Dejó de sincronizarse. No se borró nada en Drive ni aquí.");
2201
+ }
2202
+ } catch (err) {
2203
+ notify("danger", err instanceof Error ? err.message : "No se pudo completar la operación con Drive.");
2204
+ }
2205
+ };
1945
2206
  const run = async (action, ok) => {
1946
2207
  try {
1947
2208
  await action();
@@ -1998,12 +2259,24 @@ const LibraryTab = () => {
1998
2259
  if (action === "rename") return setDialog({ kind: "renameFile", file });
1999
2260
  if (action === "move") return setDialog({ kind: "moveFile", file });
2000
2261
  if (action === "delete") return setDialog({ kind: "deleteFile", file });
2262
+ if (action === "syncDrive") return toggleDrive("mark", "file", file.id);
2263
+ if (action === "unsyncDrive") return toggleDrive("unmark", "file", file.id);
2001
2264
  };
2002
2265
  const validateFolder = (parent, except) => (name) => cleanFolderName(name) !== except && nameTaken(tree, parent, name) ? "Ya existe una carpeta con ese nombre aquí." : null;
2266
+ const pendingShown = (() => {
2267
+ const inside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2268
+ const isPending = (key) => driveView(drive.links, key, inside)?.label === "Drive: pendiente";
2269
+ return files.some((f) => isPending(`file:${f.id}`)) || childrenOf(tree, current).some((f) => isPending(`folder:${f.id}`));
2270
+ })();
2271
+ useEffect(() => {
2272
+ setDrivePending(pendingShown);
2273
+ }, [pendingShown]);
2003
2274
  if (forbidden) {
2004
2275
  return /* @__PURE__ */ jsx(EmptyNote, { children: "Para usar la biblioteca necesitas permiso de la Media Library de Strapi. Pídelo a un administrador." });
2005
2276
  }
2006
2277
  const subfolders = childrenOf(tree, current);
2278
+ const currentInside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2279
+ 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
2280
  return /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "stretch", children: [
2008
2281
  /* @__PURE__ */ jsx(Box, { width: "280px", shrink: 0, background: "neutral0", padding: 3, hasRadius: true, style: { maxHeight: "calc(100vh - 220px)", overflowY: "auto" }, children: /* @__PURE__ */ jsx(
2009
2282
  FolderTree,
@@ -2050,6 +2323,9 @@ const LibraryTab = () => {
2050
2323
  ] }, f.id))
2051
2324
  ] }),
2052
2325
  /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
2326
+ folder && folderDrive?.label && /* @__PURE__ */ jsx(Badge, { variant: folderDrive.tone, title: folderDrive.title, children: folderDrive.label }),
2327
+ folder && folderDrive?.action === "mark" && /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => toggleDrive("mark", "folder", folder.id), children: "Sincronizar con Drive" }),
2328
+ folder && folderDrive?.action === "unmark" && /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: () => toggleDrive("unmark", "folder", folder.id), children: "Dejar de sincronizar" }),
2053
2329
  folder && /* @__PURE__ */ jsxs(Fragment, { children: [
2054
2330
  /* @__PURE__ */ jsx(Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsx(Pencil, {}), onClick: () => setDialog({ kind: "renameFolder" }), children: "Renombrar carpeta" }),
2055
2331
  /* @__PURE__ */ jsx(Button, { variant: "danger-light", startIcon: /* @__PURE__ */ jsx(Trash, {}), onClick: () => setDialog({ kind: "deleteFolder" }), children: "Eliminar carpeta" })
@@ -2074,11 +2350,15 @@ const LibraryTab = () => {
2074
2350
  children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
2075
2351
  /* @__PURE__ */ jsx(Folder, { width: 16, height: 16, "aria-hidden": true }),
2076
2352
  /* @__PURE__ */ jsx(Typography, { ellipsis: true, style: { flex: 1 }, children: f.name }),
2353
+ (() => {
2354
+ const view = driveView(drive.links, `folder:${f.id}`, currentInside);
2355
+ return view?.label ? /* @__PURE__ */ jsx(Badge, { size: "S", variant: view.tone, title: view.title, children: view.label }) : null;
2356
+ })(),
2077
2357
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: counts[f.id]?.files ?? 0 })
2078
2358
  ] })
2079
2359
  }
2080
2360
  ) }, 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)) }),
2361
+ 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
2362
  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
2363
  "Suelta para subir a «",
2084
2364
  folder?.name ?? ROOT_LABEL,
@@ -2760,6 +3040,15 @@ const editorReducer = (state, action) => {
2760
3040
  }
2761
3041
  }
2762
3042
  };
3043
+ const ORIGIN_URL = "/strapi-content-generator-gemini/drive/origin";
3044
+ const recordTemplateOrigin = async (post, files, template) => {
3045
+ for (const file of files) {
3046
+ try {
3047
+ await post(ORIGIN_URL, { fileId: file.id, templateDocumentId: template.documentId ?? template.id, templateName: template.name });
3048
+ } catch {
3049
+ }
3050
+ }
3051
+ };
2763
3052
  const useEditorIO = () => {
2764
3053
  const { post } = useFetchClient();
2765
3054
  const { toggleNotification } = useNotification();
@@ -2792,7 +3081,8 @@ const useEditorIO = () => {
2792
3081
  if (!node) return false;
2793
3082
  try {
2794
3083
  const blob = await renderPiece(node, template, "png");
2795
- await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3084
+ const saved = await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3085
+ await recordTemplateOrigin((u, body) => post(u, body), saved, template);
2796
3086
  toggleNotification({ type: "success", message: "Pieza guardada en la biblioteca." });
2797
3087
  return true;
2798
3088
  } catch {
@@ -2800,7 +3090,7 @@ const useEditorIO = () => {
2800
3090
  return false;
2801
3091
  }
2802
3092
  },
2803
- [api, toggleNotification]
3093
+ [api, post, toggleNotification]
2804
3094
  );
2805
3095
  return { loadImage: loadImage2, exportAs, saveToLibrary };
2806
3096
  };
@@ -5245,7 +5535,8 @@ const TemplatesTab = () => {
5245
5535
  const STUDIO_TABS = [
5246
5536
  { value: "generator", label: "Generador" },
5247
5537
  { value: "templates", label: "Plantillas" },
5248
- { value: "library", label: "Imágenes y documentos" }
5538
+ { value: "library", label: "Imágenes y documentos" },
5539
+ { value: "drive", label: "Google Drive" }
5249
5540
  ];
5250
5541
  const PANEL = { height: "100%", overflowY: "auto" };
5251
5542
  const isStudioTab = (v) => STUDIO_TABS.some((t) => t.value === v);
@@ -5273,7 +5564,8 @@ const ImageStudioTabs = () => {
5273
5564
  /* @__PURE__ */ jsxs(Box, { flex: "1", style: { minHeight: 0 }, children: [
5274
5565
  /* @__PURE__ */ jsx(Tabs.Content, { value: "generator", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(GeneratorTab, {}) }) }),
5275
5566
  /* @__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, {}) }) })
5567
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "library", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(LibraryTab, {}) }) }),
5568
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "drive", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: tab === "drive" && /* @__PURE__ */ jsx(DriveTab, {}) }) })
5277
5569
  ] })
5278
5570
  ]
5279
5571
  }