diff --git a/.gitignore b/.gitignore index 1c517ec..0075f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /dist static/buffer/ static/public/ +static/themes/ static/.env .vscode diff --git a/package-lock.json b/package-lock.json index 0039e65..9e6cae6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prss", - "version": "1.10.0", + "version": "1.11.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prss", - "version": "1.10.0", + "version": "1.11.0", "license": "GPL-3.0-or-later", "dependencies": { "@typescript-eslint/parser": "^5.8.0", diff --git a/package.json b/package.json index 43545b5..f433f26 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "prss", - "version": "1.10.0", + "version": "1.11.0", "description": "Powerful Blogging", "license": "GPL-3.0-or-later", "main": "build/index.js", diff --git a/src/common/interfaces.ts b/src/common/interfaces.ts index 6121ee1..abd9709 100644 --- a/src/common/interfaces.ts +++ b/src/common/interfaces.ts @@ -19,6 +19,22 @@ export interface ISite { menus: ISiteMenus; } +export type IManifestSiteVarsType = "string" | "image" | "url"; + +export interface IThemeManifest { + name: string; + title: string; + version: string; + author: string; + homepage: string; + license: string; + type: string; + parser: string; + templates: string[]; + siteVars: { type: IManifestSiteVarsType, description: string }[]; + isLocal?: boolean; +} + export interface IConfigThemes { [key: string]: string; } @@ -77,7 +93,7 @@ export interface ITemplateComponent { } export interface ISites { - [name: string]: ISite; + [name: string]: ISite; } export interface IPaths { @@ -89,7 +105,7 @@ export interface IStore { } export interface ISitesInternal { - [name: string]: ISiteInternal; + [name: string]: ISiteInternal; } export interface IStoreInternal { diff --git a/src/renderer/components/Addons.tsx b/src/renderer/components/Addons.tsx index 97d358e..658df43 100644 --- a/src/renderer/components/Addons.tsx +++ b/src/renderer/components/Addons.tsx @@ -101,7 +101,7 @@ const Addons: FunctionComponent = ({
- + {prssConfig.available_addons?.map(addon => { return ( diff --git a/src/renderer/components/AppSettings.tsx b/src/renderer/components/AppSettings.tsx index ff57a05..4e556e4 100644 --- a/src/renderer/components/AppSettings.tsx +++ b/src/renderer/components/AppSettings.tsx @@ -118,7 +118,7 @@ const AppSettings: FunctionComponent = ({ setHeaderLeftComponent }) => {
-
+
- +
= ({ - onSave = (h, f, s) => {} + onSave = (h, f, s) => { } }) => { const headHTMLState = useRef(null); const footerHTMLState = useRef(null); const sidebarHTMLState = useRef(null); - const [headHtmlEnabled, setHeadHtmlEnabled] = useState(false); - const [footerHtmlEnabled, setFooterHtmlEnabled] = useState(false); - const [sidebarHtmlEnabled, setSidebarHtmlEnabled] = useState(false); + const [headHtmlEnabled, setHeadHtmlEnabled] = useState(true); + const [footerHtmlEnabled, setFooterHtmlEnabled] = useState(true); + const [sidebarHtmlEnabled, setSidebarHtmlEnabled] = useState(true); const [show, setShow] = useState(false); useEffect(() => { @@ -38,8 +38,8 @@ const HTMLEditorOverlay: FunctionComponent = ({ setShow(true); }); }, []); - - if(!show){ + + if (!show) { return null; } @@ -78,42 +78,48 @@ const HTMLEditorOverlay: FunctionComponent = ({ return (
-
- - -
-

- - Head +
+ HTML Editor +
+
+ + +

- {headHtmlEnabled && ( - <> -
-
Add Raw HTML to the <HEAD>
+ +
+

+ + Head +

+ {headHtmlEnabled && ( + <> +
+
Add Raw HTML to the <HEAD>
showParametersInfo()} @@ -121,98 +127,99 @@ const HTMLEditorOverlay: FunctionComponent = ({ assistant See available parameters
-
- { - headHTMLState.current = html; - }} - name="html-editor-component" - editorProps={{ $blockScrolling: true }} - /> - - )} - -

- - Footer -

- {footerHtmlEnabled && ( - <> -
-
- Add Raw HTML to the end of the <BODY>
-
- { - footerHTMLState.current = html; - }} - name="html-editor-component" - editorProps={{ $blockScrolling: true }} - /> - - )} + { + headHTMLState.current = html; + }} + name="html-editor-component" + editorProps={{ $blockScrolling: true }} + /> + + )} -

- - Sidebar -

- {sidebarHtmlEnabled && ( - <> -
-
- If your theme supports sidebars, you can add Raw HTML to it. +

+ + Footer +

+ {footerHtmlEnabled && ( + <> +
+
+ Add Raw HTML to the end of the <BODY> +
-
- { - sidebarHTMLState.current = html; - }} - name="html-editor-component" - editorProps={{ $blockScrolling: true }} - /> - - )} + { + footerHTMLState.current = html; + }} + name="html-editor-component" + editorProps={{ $blockScrolling: true }} + /> + + )} + +

+ + Sidebar +

+ {sidebarHtmlEnabled && ( + <> +
+
+ If your theme supports sidebars, you can add Raw HTML to it. +
+
+ { + sidebarHTMLState.current = html; + }} + name="html-editor-component" + editorProps={{ $blockScrolling: true }} + /> + + )} +
); diff --git a/src/renderer/components/Modal.tsx b/src/renderer/components/Modal.tsx index 5480975..e3be914 100644 --- a/src/renderer/components/Modal.tsx +++ b/src/renderer/components/Modal.tsx @@ -102,7 +102,7 @@ class Modal extends Component { width: "101%", alignItems: "center", justifyContent: "center", - top: 0, + top: "50px", left: 0, display: "flex", zIndex: 99999999, @@ -212,7 +212,7 @@ class Modal extends Component { width: "101%", alignItems: "center", justifyContent: "center", - top: 0, + top: "50px", left: 0, display: "flex", zIndex: 99999999, @@ -312,7 +312,7 @@ class Modal extends Component { width: "101%", alignItems: "center", justifyContent: "center", - top: 0, + top: "50px", left: 0, display: "flex", zIndex: 99999999, diff --git a/src/renderer/components/PRSSAI.tsx b/src/renderer/components/PRSSAI.tsx index 3d56bfb..648dfd3 100644 --- a/src/renderer/components/PRSSAI.tsx +++ b/src/renderer/components/PRSSAI.tsx @@ -392,7 +392,7 @@ const PRSSAI: FunctionComponent = ({
- + { storeInt.set("prssaiLastActiveSection", activeKey); diff --git a/src/renderer/components/PostEditor.tsx b/src/renderer/components/PostEditor.tsx index 5b7a420..2c464fe 100644 --- a/src/renderer/components/PostEditor.tsx +++ b/src/renderer/components/PostEditor.tsx @@ -503,6 +503,10 @@ const PostEditor: FunctionComponent = ({ setHeaderLeftComponent }) => { setStatusMessage("success", "Post updated"); }, []); + const handleFeaturedImageSet = useCallback(() => { + setStatusMessage("success", "Post updated"); + }, []); + const onEditorKeyPress = (e: KeyboardEvent) => { if (e) { if (e.ctrlKey) { @@ -621,6 +625,7 @@ const PostEditor: FunctionComponent = ({ setHeaderLeftComponent }) => { onOpenRawHTMLOverlay={openRawHTMLOverlay} onOpenVarEditorOverlay={openVariablesOverlay} onToggleRawHTMLOnly={toggleRawHTMLOnly} + onFeaturedImageSet={handleFeaturedImageSet} />
diff --git a/src/renderer/components/PostEditorSidebar.tsx b/src/renderer/components/PostEditorSidebar.tsx index 912a182..0be8494 100644 --- a/src/renderer/components/PostEditorSidebar.tsx +++ b/src/renderer/components/PostEditorSidebar.tsx @@ -2,7 +2,7 @@ import "./styles/PostEditorSidebar.css"; import React, { FunctionComponent, useState, Fragment, useEffect } from "react"; import cx from "classnames"; -import { noop, confirmation } from "../services/utils"; +import { noop, uploadAssetImage, removeAssetImage } from "../services/utils"; import Loading from "./Loading"; import { configGet, getString } from "../../common/utils"; import { modal } from "./Modal"; @@ -10,6 +10,7 @@ import { getTemplateList } from "../services/theme"; import { IPostItem, ISite, Noop } from "../../common/interfaces"; import { setHook } from "../../common/bootstrap"; import { isPreviewActive } from "../services/preview"; +import { updateItem } from "../services/db"; interface IProps { site: ISite; @@ -24,6 +25,7 @@ interface IProps { onOpenRawHTMLOverlay?: Noop; onOpenVarEditorOverlay?: Noop; onToggleRawHTMLOnly?: Noop; + onFeaturedImageSet?: Noop; } const PostEditorSidebar: FunctionComponent = ({ @@ -35,12 +37,14 @@ const PostEditorSidebar: FunctionComponent = ({ onStopPreview = noop, onStartPreview = noop, onPublish = noop, - onChangePostTemplate = (t) => {}, + onChangePostTemplate = (t) => { }, onOpenRawHTMLOverlay = noop, onOpenVarEditorOverlay = noop, onToggleRawHTMLOnly = noop, + onFeaturedImageSet = noop, }) => { const themeName = site.theme; + const [updatedItem, setUpdatedItem] = useState(item); const [deployLoading, setDeployLoading] = useState(false); const [buildLoading, setBuildLoading] = useState(false); const [buildAllLoading, setBuildAllLoading] = useState(false); @@ -89,26 +93,80 @@ const PostEditorSidebar: FunctionComponent = ({ setHook("PostEditorSidebar_deployLoading", (value: boolean) => { setDeployLoading(value); }); + + setHook("PostEditorSidebar_setUpdatedItem", (value: IPostItem) => { + setUpdatedItem(value); + }); }, []); if (!templateList) { return null; } - const toggleForceRawHTML = async () => { - if (forceRawHTMLEditing) { - const confirmationRes = await confirmation({ - title: getString("warn_force_raw_html_disable"), + const setFeaturedImage = async () => { + if (!updatedItem) { + return; + } + + const filePath = await uploadAssetImage(site.name); + + if (filePath) { + // Update post vars + const newItem = { + ...updatedItem, + vars: { + ...updatedItem.vars, + featuredImageUrl: filePath + }, + updatedAt: Date.now() + }; + + /** + * Update item + */ + await updateItem(site.uuid, item.uuid, { + vars: { + ...item.vars, + featuredImageUrl: filePath + }, + updatedAt: Date.now() }); - if (confirmationRes !== 0) { - modal.alert(["action_cancelled", []]); - return; - } + item = newItem; + setUpdatedItem(newItem); + onFeaturedImageSet(); } + } + + const removeFeaturedImage = async () => { + if (!updatedItem) { + return; + } + + const updatedVars = { ...updatedItem.vars }; + await removeAssetImage(site.name, updatedVars.featuredImageUrl); - onToggleRawHTMLOnly(); - }; + delete updatedVars.featuredImageUrl; + + // Update post vars + const newItem = { + ...updatedItem, + vars: updatedVars, + updatedAt: Date.now() + }; + + /** + * Update item + */ + await updateItem(site.uuid, updatedItem.uuid, { + vars: updatedVars, + updatedAt: Date.now() + }); + + item = newItem; + setUpdatedItem(newItem); + onFeaturedImageSet(); + } const buildStr = previewStarted ? "Save & Build" : "Save"; @@ -244,6 +302,7 @@ const PostEditorSidebar: FunctionComponent = ({
+
  • onOpenRawHTMLOverlay()}> code{" "} Add Raw HTML code @@ -252,23 +311,18 @@ const PostEditorSidebar: FunctionComponent = ({ create{" "} Edit Variables
  • - {/*
  • -
    - toggleForceRawHTML()} - /> - -
    -
  • */} + + {updatedItem?.vars.featuredImageUrl ? ( +
  • removeFeaturedImage()}> + close{" "} + Remove Featured Image +
  • + ) : ( +
  • setFeaturedImage()}> + image{" "} + Set Featured Image +
  • + )} )} diff --git a/src/renderer/components/SiteSettings.tsx b/src/renderer/components/SiteSettings.tsx index 24845de..cb42cf2 100644 --- a/src/renderer/components/SiteSettings.tsx +++ b/src/renderer/components/SiteSettings.tsx @@ -236,7 +236,7 @@ const SiteSettings: FunctionComponent = ({
    - + Site ID diff --git a/src/renderer/components/SiteVariablesEditorOverlay.tsx b/src/renderer/components/SiteVariablesEditorOverlay.tsx index 0a95b78..c7f7177 100644 --- a/src/renderer/components/SiteVariablesEditorOverlay.tsx +++ b/src/renderer/components/SiteVariablesEditorOverlay.tsx @@ -11,7 +11,7 @@ import React, { import { Link } from "react-router-dom"; import cx from "classnames"; -import { camelCase } from "../services/utils"; +import { camelCase, removeAssetImage, uploadAssetImage } from "../services/utils"; import "ace-builds/webpack-resolver"; import "ace-builds/src-noconflict/mode-html"; @@ -20,9 +20,10 @@ import { toast } from "react-toastify"; import { modal } from "./Modal"; import { siteVarToArray } from "../services/hosting"; import { getBufferItems } from "../services/build"; -import { getItems, updateSite, updateItem } from "../services/db"; -import { IPostItem, ISite } from "../../common/interfaces"; -import { setHook } from "../../common/bootstrap"; +import { getItems, updateSite, updateItem, getSite } from "../services/db"; +import { IPostItem, ISite, IThemeManifest } from "../../common/interfaces"; +import { runHook, setHook } from "../../common/bootstrap"; +import { getThemeManifest } from "../services/theme"; interface IProps { site: ISite; @@ -30,6 +31,8 @@ interface IProps { onSave: () => void; } +type IVarsKV = { name: string, content: string, type?: string }; + const SiteVariablesEditorOverlay: FunctionComponent = ({ site, post, @@ -37,15 +40,17 @@ const SiteVariablesEditorOverlay: FunctionComponent = ({ }) => { const items = useRef(null); + const [updatedSite, setUpdatedSite] = useState(site); const [bufferItem, setBufferItem] = useState(null); const [show, setShow] = useState(false); - const [parsedVariables, setParsedVariables] = useState<{ [key: string]: string }[]>([]); - const [parsedInheritedVariables, setParsedInheritedVariables] = useState<{ [key: string]: string }[]>([]); + const [parsedVariables, setParsedVariables] = useState([]); + const [parsedInheritedVariables, setParsedInheritedVariables] = useState([]); - const [variables, setVariables] = useState<{ [key: string]: string }[]>(parsedVariables); - const [exclusiveVariables, setExclusiveVariables] = useState([]); + const [themeManifest, setThemeManifest] = useState(null); + const [suggestedVariables, setSuggestedVariables] = useState([]); - const variablesBuffer = useRef(parsedVariables) as any; + const [variables, setVariables] = useState(parsedVariables); + const [exclusiveVariables, setExclusiveVariables] = useState([]); useEffect(() => { setHook("SiteVariablesEditorOverlay_show", async (value: string) => { @@ -54,20 +59,22 @@ const SiteVariablesEditorOverlay: FunctionComponent = ({ }, []); const setData = useCallback(async () => { - const itemsRes = await getItems(site.uuid); + const siteRes = await getSite(site.uuid); + const itemsRes = await getItems(siteRes.uuid); + const currentItem = post ? itemsRes.find((item) => item.uuid === post.uuid) : null; + const baseVars = currentItem ? currentItem.vars || {} : siteRes.vars || {}; + items.current = itemsRes; - const bufferItems = await getBufferItems(site); + const bufferItems = await getBufferItems(siteRes); const bufferItem = - bufferItems && post - ? bufferItems.find((bufferItem) => bufferItem.item.uuid === post.uuid) + bufferItems && currentItem + ? bufferItems.find((bufferItem) => bufferItem.item.uuid === currentItem.uuid) : null; setBufferItem(bufferItem); - const baseVars = post ? post.vars || {} : site.vars || {}; - const parsedVariables = siteVarToArray( Object.keys(baseVars).length ? baseVars : { "": "" } ); @@ -75,18 +82,40 @@ const SiteVariablesEditorOverlay: FunctionComponent = ({ setParsedVariables(parsedVariables); setVariables(parsedVariables); - const exclusiveVariables = post ? post.exclusiveVars || [] : []; + const exclusiveVariables = currentItem ? currentItem.exclusiveVars || [] : []; setExclusiveVariables(exclusiveVariables); - variablesBuffer.current = variablesBuffer; - bufferItem && - setParsedInheritedVariables(siteVarToArray(bufferItem.vars)); + if (bufferItem) { + const bufferItemVars = siteVarToArray(bufferItem.vars); + setParsedInheritedVariables(bufferItemVars); + } + + // Get theme vars + if (siteRes.theme) { + const manifest = await getThemeManifest(siteRes.theme); + // Add to parsed variables >> parsedVariables + // Must not be in computed variables + let suggestedVars = []; + + if (manifest.siteVars) { + Object.keys(manifest.siteVars).forEach(manifestSiteVarName => { + suggestedVars.push(manifestSiteVarName); + }); + + if (suggestedVars.length) { + setThemeManifest(manifest); + setSuggestedVariables(suggestedVars); + } + } + } + + setUpdatedSite(siteRes); setShow(true); }, []); - const addNew = () => { - setVariables((prevVars) => [...prevVars, { name: "", content: "" }]); + const addNew = (input?: IVarsKV) => { + setVariables((prevVars) => [(input || { name: "", content: "" }), ...prevVars]); }; const setVar = ( @@ -103,16 +132,48 @@ const SiteVariablesEditorOverlay: FunctionComponent = ({ [fieldName]: isNormalized ? camelCase(e.target.value) : e.target.value, }; - variablesBuffer; setVariables(newVars); } }; - const delVar = (varIndex: number) => { + const delVar = async (varIndex: number) => { const newVars = [...variables]; delete newVars[varIndex]; - return setVariables([...newVars.filter((variable) => !!variable)]); + // If type is image and local path is used, we need to remove local image and update the site + if(themeManifest.siteVars?.[variables[varIndex].name]?.type === "image"){ + await removeAssetImage(site.name, variables[varIndex].content); + + // Update site or post + const siteRes = await getSite(site.uuid); + const itemsRes = await getItems(siteRes.uuid); + const currentItem = post ? itemsRes.find((item) => item.uuid === post.uuid) : null; + const baseVars = currentItem ? currentItem.vars || {} : siteRes.vars || {}; + + delete baseVars[variables[varIndex].name]; + await handleSave(siteVarToArray(baseVars)); + } + + setVariables([...newVars.filter((variable) => !!variable)]); + }; + + + const uploadImage = async ( + varIndex: number + ) => { + const newVars = [...variables]; + + if (varIndex > -1) { + const filePath = await uploadAssetImage(site.name); + + // Update variable + newVars[varIndex] = { + ...newVars[varIndex], + content: filePath, + }; + + setVariables([...newVars]); + } }; const preventVarToggle = (varIndex: number) => { @@ -138,13 +199,17 @@ const SiteVariablesEditorOverlay: FunctionComponent = ({ } }; - const handleSave = async () => { + const handleSave = async (inputVars = variables) => { const updatedAt = Date.now(); /** * Removing empty vars */ - const varsArray = variables.filter((varItem) => !!varItem.name.trim()); + const varsArray = inputVars.filter((varItem) => { + const emptyName = !varItem.name.trim(); + //const emptyValAndSuggested = varItem.name && suggestedVariables.includes(varItem.name) && !varItem.content.trim(); + return !emptyName/* && !emptyValAndSuggested*/; + }); /** * Removing orphan exclusiveVars or duplicated @@ -183,15 +248,16 @@ const SiteVariablesEditorOverlay: FunctionComponent = ({ /** * Update item */ - await updateItem(site.uuid, post.uuid, { + await updateItem(updatedSite.uuid, post.uuid, { vars: varObj, exclusiveVars: newExclusiveVarsArr, updatedAt, }); post = updatedItem; - + onSave(); + runHook("PostEditorSidebar_setUpdatedItem", updatedItem); } else { /** * Save to site @@ -205,33 +271,48 @@ const SiteVariablesEditorOverlay: FunctionComponent = ({ /** * Update site updatedAt */ - await updateSite(site.uuid, { + await updateSite(updatedSite.uuid, { vars: varObj, updatedAt, }); site = updatedSite; - toast.success("Site variables saved!"); + + if(inputVars === variables){ + toast.success("Site variables saved!"); + } } }; + const getVarIcon = useCallback((name: string, type = "string") => { + if (type === "image") { + return "image"; + } else if (type === "url" || name.toLowerCase().includes("url")) { + return "link"; + } else if (name.toLowerCase().includes("html")) { + return "code"; + } else { + return "text_fields" + } + }, []); + const showInfo = () => { modal.alert(

    Site Variables are variables that your templates can use.

    - For example: headerImageUrl + For example: featuredImageUrl

    This variable would be used by some templates as a header image url.

    -

    Each template generally documents the siteVars it uses.

    +

    To see the available variables, check out the "Suggested Variables" section.

    - Note: A variable defined at the post level will override one set at + Note: A variable defined at the post level will override one set at the site level.

    - Note 2: Variables are published to your site and therefore public. Do + Note #2: Variables are published to your site and therefore public. Do not store sensitive data in variables.

    , @@ -255,7 +336,7 @@ const SiteVariablesEditorOverlay: FunctionComponent = ({

    - Scoped Variables + Variables Editor

    - -
    -
    - Add, edit or delete variables -
    -
    showInfo()} - > - info - What are variables? -
    -
    - -
    -
      - {variables.map((variable, index) => { - const isRestricted = exclusiveVariables.includes(variable.name); - - return ( -
    • +
      +
      +

      +
      + Scoped +
      +
      showInfo()} > -
      - setVar(e, index, "name")} - onBlur={(e) => setVar(e, index, "name", true)} - /> - setVar(e, index, "content")} - /> - {post && ( -
      +

      +
      +
      + Add, edit or delete variables for this page and descendants. +
      +
      +
      +
        + {variables.map((variable, index) => { + const isRestricted = exclusiveVariables.includes(variable.name); + + return ( +
      • - block - - )} - -
      -
    • - ); - })} -
    -
    - - {post && !!parsedInheritedVariables.length && ( - <> -

    Computed Variables

    -

    - This includes global variables from{" "} - Site Settings as - well as parent variables. -

    -
    -
      - {parsedInheritedVariables.map((variable, index) => { - return ( -
    • -
      - - -
      -
    • - ); - })} -
    +
    + + {getVarIcon(variable.name, themeManifest.siteVars?.[variable.name]?.type)} + +
    +