Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Export to image #9037

Merged
merged 55 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
3ecb9d7
feat (wip): export to image
sprocketc Apr 4, 2023
4f36256
feat (wip): export to image
sprocketc Apr 5, 2023
b36a76d
chore: add comment
sprocketc Apr 6, 2023
63dc9c8
Merge branch 'master' into feat/export-to-image
sprocketc Apr 6, 2023
df6cfe2
chore: export selection on whiteboards
sprocketc Apr 6, 2023
de2a6c4
fix: add dependency
sprocketc Apr 6, 2023
92cf046
refactor: remove unused function
sprocketc Apr 6, 2023
82e3235
fix: whiteboards zoom on export
sprocketc Apr 6, 2023
cf5c19c
fix: loading position
sprocketc Apr 6, 2023
55edf47
chore: support video thumb
sprocketc Apr 6, 2023
09aa51b
Merge branch 'master' into feat/export-to-image
sprocketc Apr 7, 2023
8330045
core: add export to whiteboards context menu
sprocketc Apr 7, 2023
afe173c
fix: context menu entry
sprocketc Apr 7, 2023
b9a895d
fix; copy image to clipboard
sprocketc Apr 7, 2023
d8ba6bc
fix: copy / export label
sprocketc Apr 7, 2023
91f8f5b
fix: hide ui elements
sprocketc Apr 7, 2023
8d737e2
fix: remove random character
sprocketc Apr 7, 2023
9e85fec
fix: graph export
sprocketc Apr 7, 2023
bfe746b
chore: remove console log and jpg format
sprocketc Apr 7, 2023
f5fcba4
style: run prettier
sprocketc Apr 7, 2023
9091244
fix: disable on multiple selected blocks
sprocketc Apr 7, 2023
07bab4f
fix: multiple blocks
sprocketc Apr 7, 2023
3e5b8ef
enhance: restrict bounds of selected shapes
sprocketc Apr 7, 2023
7c016a7
Merge branch 'master' into feat/export-to-image
sprocketc Apr 7, 2023
700ed1c
feat (wip): export to image
sprocketc Apr 4, 2023
951ab08
feat (wip): export to image
sprocketc Apr 5, 2023
5b62977
chore: add comment
sprocketc Apr 6, 2023
9ed86bd
chore: export selection on whiteboards
sprocketc Apr 6, 2023
77cb2a8
fix: add dependency
sprocketc Apr 6, 2023
3186d86
refactor: remove unused function
sprocketc Apr 6, 2023
8fa22ef
fix: whiteboards zoom on export
sprocketc Apr 6, 2023
dea3495
fix: loading position
sprocketc Apr 6, 2023
454bbcf
chore: support video thumb
sprocketc Apr 6, 2023
1fd2a0c
core: add export to whiteboards context menu
sprocketc Apr 7, 2023
6280343
fix: context menu entry
sprocketc Apr 7, 2023
2c1859c
fix; copy image to clipboard
sprocketc Apr 7, 2023
edf0e59
fix: copy / export label
sprocketc Apr 7, 2023
965b94c
fix: hide ui elements
sprocketc Apr 7, 2023
5f86ea0
fix: remove random character
sprocketc Apr 7, 2023
da4167e
fix: graph export
sprocketc Apr 7, 2023
4ae7921
chore: remove console log and jpg format
sprocketc Apr 7, 2023
9a09a5c
style: run prettier
sprocketc Apr 7, 2023
a16b5d8
fix: disable on multiple selected blocks
sprocketc Apr 7, 2023
f0d802d
fix: multiple blocks
sprocketc Apr 7, 2023
b841811
enhance: restrict bounds of selected shapes
sprocketc Apr 7, 2023
d1a4199
Fix any html2canvas related functionality failing in publishing
logseq-cldwalker Apr 10, 2023
3452ea8
Merge branch 'feat/export-to-image' of github.com:logseq/logseq into …
sprocketc Apr 11, 2023
84bd8ef
fix: portal header gradient on export
sprocketc Apr 11, 2023
5bad0e2
chore: add comment about html2canvas-ignore attr
sprocketc Apr 11, 2023
e5c4cda
fix: use export padding constant
sprocketc Apr 11, 2023
28ff95f
fix: export collapsed portals with size > medium
sprocketc Apr 11, 2023
9e60565
Merge branch 'master' into feat/export-to-image
tiensonqin Apr 12, 2023
c738e35
fix: reset export type
sprocketc Apr 12, 2023
33ab571
enhance: export filename
sprocketc Apr 12, 2023
1543844
Merge branch 'master' into feat/export-to-image
tiensonqin Apr 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion deps/publishing/src/logseq/publishing/html.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ necessary db filtering"
}(window.location))"]
;; TODO: should make this configurable
[:script {:src "static/js/main.js"}]
[:script {:src "static/js/highlight.min.js"}]
[:script {:src "static/js/interact.min.js"}]
[:script {:src "static/js/highlight.min.js"}]
[:script {:src "static/js/katex.min.js"}]
[:script {:src "static/js/html2canvas.min.js"}]
[:script {:src "static/js/code-editor.js"}]])))))

(defn build-html
Expand Down
1 change: 1 addition & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
</script>
<script defer src="/static/js/highlight.min.js"></script>
<script defer src="/static/js/interact.min.js"></script>
<script defer src="/static/js/html2canvas.min.js"></script>
logseq-cldwalker marked this conversation as resolved.
Show resolved Hide resolved
<script defer src="/static/js/main.js"></script>
<script defer src="/static/js/amplify.js"></script>
<script defer src="/static/js/tabler.min.js"></script>
Expand Down
5 changes: 5 additions & 0 deletions resources/css/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -921,4 +921,9 @@ html.is-mobile {
@apply grid grid-flow-col auto-cols-max;
place-items: center;
}

/* fixes an html2canvas issue */
img {
@apply inline-block;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor tweaks to the existing styles might be required to improve the result.

}
1 change: 1 addition & 0 deletions resources/electron.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
</script>
<script defer src="./js/highlight.min.js"></script>
<script defer src="./js/interact.min.js"></script>
<script defer src="./js/html2canvas.min.js"></script>
<script defer src="./js/lsplugin.core.js"></script>
<script defer src="./js/main.js"></script>
<script defer src="./js/amplify.js"></script>
Expand Down
1 change: 1 addition & 0 deletions resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
</script>
<script defer src="./js/highlight.min.js"></script>
<script defer src="./js/interact.min.js"></script>
<script defer src="./js/html2canvas.min.js"></script>
<script defer src="./js/lsplugin.core.js"></script>
<script defer src="./js/main.js"></script>
<script defer src="./js/amplify.js"></script>
Expand Down
20 changes: 20 additions & 0 deletions resources/js/html2canvas.min.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions src/main/frontend/components/content.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
:on-click (fn [_]
(let [block-uuids (editor-handler/get-selected-toplevel-block-uuids)]
(state/set-modal!
#(export/export-blocks block-uuids))))}
(t :content/copy-as)
#(export/export-blocks block-uuids {:whiteboard? false}))))}
(t :content/copy-export-as)
nil)
(ui/menu-link
{:key "copy block refs"
Expand Down Expand Up @@ -218,8 +218,8 @@
(ui/menu-link
{:key "Copy as"
:on-click (fn [_]
(state/set-modal! #(export/export-blocks [block-id])))}
(t :content/copy-as)
(state/set-modal! #(export/export-blocks [block-id] {:whiteboard? false})))}
(t :content/copy-export-as)
nil)

(ui/menu-link
Expand Down
320 changes: 198 additions & 122 deletions src/main/frontend/components/export.cljs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/main/frontend/components/export.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
.export textarea, .export select {
background: var(--ls-primary-background-color);
}

#export-preview {
max-height: 50vh;
background-color: #fff;
background-image: linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
13 changes: 12 additions & 1 deletion src/main/frontend/components/page.cljs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns frontend.components.page
(:require [clojure.string :as string]
(:require ["/frontend/utils" :as utils]
[clojure.string :as string]
[frontend.components.block :as component-block]
[frontend.components.query :as query]
[frontend.components.content :as content]
Expand Down Expand Up @@ -638,6 +639,16 @@
"Clear All"]]
[:a.opacity-70.opacity-100 {:on-click #(route-handler/go-to-search! :graph)}
"Click to search"])]))
{:search-filters search-graph-filters})
(graph-filter-section
[:span.font-medium "Export"]
(fn [open?]
(filter-expand-area
open?
(when-let [canvas (js/document.querySelector "#global-graph canvas")]
[:div.p-6
;; We'll get an empty image if we don't wrap this in a requestAnimationFrame
[:div [:a {:on-click #(.requestAnimationFrame js/window (fn [] (utils/canvasToImage canvas "graph" "png")))} "as PNG"]]])))
{:search-filters search-graph-filters})]]]]))

(defonce last-node-position (atom nil))
Expand Down
4 changes: 2 additions & 2 deletions src/main/frontend/components/page_menu.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,11 @@
{:title (t :page/open-with-default-app)
:options {:on-click #(js/window.apis.openPath file-fpath)}}]))

(when (state/get-current-page)
(when (or (state/get-current-page) whiteboard?)
{:title (t :export-page)
:options {:on-click #(state/set-modal!
(fn []
(export/export-blocks (:block/name page))))}})
(export/export-blocks (:block/name page) {:whiteboard? whiteboard?})))}})

(when (util/electron?)
{:title (t (if public? :page/make-private :page/make-public))
Expand Down
1 change: 1 addition & 0 deletions src/main/frontend/components/whiteboard.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
:-webkit-font-smoothing "subpixel-antialiased"}}

[:div.whiteboard-page-title-root
{:data-html2canvas-ignore true} ; excludes title component from image export
[:div.whiteboard-page-title
{:style {:color "var(--ls-primary-text-color)"
:user-select "none"}
Expand Down
2 changes: 1 addition & 1 deletion src/main/frontend/dicts.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
:dev/show-block-data "(Dev) Show block data"
:dev/show-block-ast "(Dev) Show block AST"
:dev/show-page-ast "(Dev) Show page AST"
:content/copy-as "Copy as..."
:content/copy-export-as "Copy / Export as.."
:content/copy-block-url "Copy block URL"
:content/copy-block-ref "Copy block ref"
:content/copy-block-emebed "Copy block embed"
Expand Down
2 changes: 2 additions & 0 deletions src/main/frontend/extensions/tldraw.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"Adapters related to tldraw"
(:require ["/frontend/tldraw-logseq" :as TldrawLogseq]
[frontend.components.block :as block]
[frontend.components.export :as export]
[frontend.components.page :as page]
[frontend.config :as config]
[frontend.db.model :as model]
Expand Down Expand Up @@ -90,6 +91,7 @@
(clj->js
(model/query-block-by-uuid (parse-uuid block-uuid))))
:getBlockPageName #(:block/name (model/get-block-page (state/get-current-repo) (parse-uuid %)))
:exportToImage (fn [page-name options] (state/set-modal! #(export/export-blocks page-name (merge (js->clj options :keywordize-keys true) {:whiteboard? true}))))
:isWhiteboardPage model/whiteboard-page?
:isMobile util/mobile?
:saveAsset save-asset-handler
Expand Down
19 changes: 19 additions & 0 deletions src/main/frontend/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,25 @@ export const toPosixPath = (input) => {
return input && input.replace(/\\+/g, '/')
}

export const saveToFile = (data, fileName, format) => {
if (!data) return
const url = URL.createObjectURL(data)
const link = document.createElement('a')
link.href = url
link.download = `${fileName}.${format}`
link.click()
}

export const canvasToImage = (canvas, title = 'Untitled', format = 'png') => {
canvas.toBlob(
(blob) => {
console.log(blob)
saveToFile(blob, title, format)
},
`image/.${format}`
)
}

export const nodePath = Object.assign({}, path, {
basename (input) {
input = toPosixPath(input)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const ActionBar = observer(function ActionBar(): JSX.Element {
}, [app])

return (
<div className="tl-action-bar">
<div className="tl-action-bar" data-html2canvas-ignore="true">
{!app.readOnly && (
<div className="tl-toolbar tl-history-bar">
<Button tooltip="Undo" onClick={undo}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const CircleButton = ({
<button
data-active={active}
data-recently-changed={recentlyChanged}
data-html2canvas-ignore="true"
style={style}
className="tl-circle-button"
onPointerDown={onClick}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useApp } from '@tldraw/react'
import { MOD_KEY, AlignType, DistributeType, isDev } from '@tldraw/core'
import { LogseqContext } from '../../lib/logseq-context'
import { MOD_KEY, AlignType, DistributeType, isDev, EXPORT_PADDING } from '@tldraw/core'
import { observer } from 'mobx-react-lite'
import { TablerIcon } from '../icons'
import { Button } from '../Button'
Expand All @@ -19,6 +20,7 @@ export const ContextMenu = observer(function ContextMenu({
collisionRef,
}: ContextMenuProps) {
const app = useApp()
const { handlers } = React.useContext(LogseqContext)
const rContent = React.useRef<HTMLDivElement>(null)

const runAndTransition = (f: Function) => {
Expand Down Expand Up @@ -234,6 +236,27 @@ export const ContextMenu = observer(function ContextMenu({
</ReactContextMenu.Item>
)}
<ReactContextMenu.Separator className="menu-separator" />
<ReactContextMenu.Item
className="tl-menu-item"
onClick={() =>
runAndTransition(() =>
handlers.exportToImage(app.currentPageId, {
x: app.selectionBounds.minX + app.viewport.camera.point[0] - EXPORT_PADDING,
y: app.selectionBounds.minY + app.viewport.camera.point[1] - EXPORT_PADDING,
width: app.selectionBounds?.width + EXPORT_PADDING * 2,
height: app.selectionBounds?.height + EXPORT_PADDING * 2,
zoom: app.viewport.camera.zoom,
})
)
}
>
<TablerIcon className="tl-menu-icon" name="file-export" />
Export
<div className="tl-menu-right-slot">
<span className="keyboard-shortcut"></span>
</div>
</ReactContextMenu.Item>
<ReactContextMenu.Separator className="menu-separator" />
<ReactContextMenu.Item
className="tl-menu-item"
onClick={() => runAndTransition(app.api.selectAll)}
Expand Down Expand Up @@ -311,7 +334,6 @@ export const ContextMenu = observer(function ContextMenu({
</span>
</div>
</ReactContextMenu.Item>
)
</>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const PrimaryTools = observer(function PrimaryTools() {
}, [])

return (
<div className="tl-primary-tools">
<div className="tl-primary-tools" data-html2canvas-ignore="true">
<div className="tl-toolbar tl-tools-floating-panel">
<ToolButton tooltip="Select" id="select" icon="select-cursor" />
<ToolButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Shape } from '../../lib'
export const StatusBar = observer(function StatusBar() {
const app = useApp<Shape>()
return (
<div className="tl-statusbar">
<div className="tl-statusbar" data-html2canvas-ignore="true">
{app.selectedTool.id} | {app.selectedTool.currentState.id}
<div style={{ flex: 1 }} />
<div id="tl-statusbar-anchor" className="flex gap-1" />
Expand Down
1 change: 1 addition & 0 deletions tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface LogseqContextValue {
filters: { 'pages?': boolean; 'blocks?': boolean; 'files?': boolean }
) => Promise<SearchResult>
addNewWhiteboard: (pageName: string) => void
exportToImage: (pageName: string, options: object) => void
addNewBlock: (content: string) => string // returns the new block uuid
queryBlockByUUID: (uuid: string) => any
getBlockPageName: (uuid: string) => string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ const LogseqPortalShapeHeader = observer(
? getComputedColor(fill, 'background')
: 'var(--ls-tertiary-background-color)'

const fillGradient =
fill && fill !== 'var(--ls-secondary-background-color)'
? `var(--ls-highlight-color-${fill})`
: 'var(--ls-secondary-background-color)'

return (
<div
className={`tl-logseq-portal-header tl-logseq-portal-header-${
Expand All @@ -72,7 +77,7 @@ const LogseqPortalShapeHeader = observer(
className="absolute inset-0 tl-logseq-portal-header-bg"
style={{
opacity,
background: type === 'P' ? bgColor : `linear-gradient(0deg, transparent, ${bgColor}`,
background: type === 'P' ? bgColor : `linear-gradient(0deg, ${fillGradient}, ${bgColor})`,
}}
></div>
<div className="relative">{children}</div>
Expand Down
4 changes: 3 additions & 1 deletion tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { action, computed } from 'mobx'
import { observer } from 'mobx-react-lite'
import { withClampedStyles } from './style-props'

export const YOUTUBE_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
export const YOUTUBE_REGEX =
/^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/

export interface YouTubeShapeProps extends TLBoxShapeProps {
type: 'youtube'
Expand Down Expand Up @@ -58,6 +59,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
style={{
pointerEvents: isEditing ? 'all' : 'none',
userSelect: 'none',
background: `url('https://img.youtube.com/vi/${this.embedId}/mqdefault.jpg') no-repeat center/cover`,
}}
>
{this.embedId ? (
Expand Down
5 changes: 1 addition & 4 deletions tldraw/apps/tldraw-logseq/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ html[data-theme='light'] {

.tl-container {
overflow: hidden;
background-color: transparent !important;
}

.tl-menu-item {
Expand Down Expand Up @@ -725,10 +726,6 @@ button.tl-select-input-trigger {
user-select: text;
transform-origin: top left;

&[data-collapsed='true'][data-editing='false'] {
@apply overflow-hidden;
}

&[data-portal-selected='true'] {
filter: brightness(0.9) contrast(0.5);
}
Expand Down
2 changes: 2 additions & 0 deletions tldraw/packages/core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const ZOOM_UPDATE_FACTOR = 0.8

export const GRID_SIZE = 8

export const EXPORT_PADDING = 8

export const EMPTY_OBJECT: any = {}

export const EMPTY_ARRAY: any[] = []
Expand Down
12 changes: 10 additions & 2 deletions tldraw/packages/react/src/components/Canvas/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
{showGrid && components.Grid && <components.Grid size={gridSize} />}
<HTMLLayer>
{components.SelectionBackground && selectedShapes && selectionBounds && showSelection && (
<Container data-type="SelectionBackground" bounds={selectionBounds} zIndex={2}>
<Container
data-type="SelectionBackground"
bounds={selectionBounds}
zIndex={2}
data-html2canvas-ignore="true"
>
<components.SelectionBackground
shapes={selectedShapes}
bounds={selectionBounds}
Expand Down Expand Up @@ -184,6 +189,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
{showSelection && components.SelectionForeground && (
<Container
data-type="SelectionForeground"
data-html2canvas-ignore="true"
bounds={selectionBounds}
zIndex={editingShape && selectedShapes.includes(editingShape) ? 1002 : 10002}
>
Expand All @@ -198,6 +204,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
{showHandles && onlySelectedShapeWithHandles && components.Handle && (
<Container
data-type="onlySelectedShapeWithHandles"
data-html2canvas-ignore="true"
bounds={selectionBounds}
zIndex={10003}
>
Expand All @@ -217,6 +224,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
{selectedShapes && components.SelectionDetail && (
<SelectionDetailContainer
key={'detail' + selectedShapes.map(shape => shape.id).join('')}
data-html2canvas-ignore="true"
shapes={selectedShapes}
bounds={selectionBounds}
detail={showSelectionRotation ? 'rotation' : 'size'}
Expand All @@ -235,7 +243,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
/>
)}

<div id="tl-dev-tools-canvas-anchor" />
<div id="tl-dev-tools-canvas-anchor" data-html2canvas-ignore="true" />
</div>
<HTMLLayer>
{selectedShapes && selectionBounds && (
Expand Down