Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
f7955437
Commit
f7955437
authored
May 12, 2024
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: retire share dialog
parent
eda19839
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
10 additions
and
379 deletions
+10
-379
MemoActionMenu.tsx
web/src/components/MemoActionMenu.tsx
+10
-5
ShareMemoDialog.tsx
web/src/components/ShareMemoDialog.tsx
+0
-184
convertResourceToDataURL.ts
web/src/labs/html2image/convertResourceToDataURL.ts
+0
-22
getCloneStyledElement.ts
web/src/labs/html2image/getCloneStyledElement.ts
+0
-40
index.ts
web/src/labs/html2image/index.ts
+0
-106
waitImageLoaded.ts
web/src/labs/html2image/waitImageLoaded.ts
+0
-17
share-memo-dialog.less
web/src/less/share-memo-dialog.less
+0
-5
No files found.
web/src/components/MemoActionMenu.tsx
View file @
f7955437
import
{
Dropdown
,
Menu
,
MenuButton
,
MenuItem
}
from
"@mui/joy"
;
import
{
Dropdown
,
Menu
,
MenuButton
,
MenuItem
}
from
"@mui/joy"
;
import
clsx
from
"clsx"
;
import
clsx
from
"clsx"
;
import
copy
from
"copy-to-clipboard"
;
import
toast
from
"react-hot-toast"
;
import
toast
from
"react-hot-toast"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
Icon
from
"@/components/Icon"
;
import
Icon
from
"@/components/Icon"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
{
extractMemoIdFromName
,
useMemoStore
}
from
"@/store/v1"
;
import
{
useMemoStore
}
from
"@/store/v1"
;
import
{
RowStatus
}
from
"@/types/proto/api/v1/common"
;
import
{
RowStatus
}
from
"@/types/proto/api/v1/common"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
showCommonDialog
}
from
"./Dialog/CommonDialog"
;
import
{
showCommonDialog
}
from
"./Dialog/CommonDialog"
;
import
showMemoEditorDialog
from
"./MemoEditor/MemoEditorDialog"
;
import
showMemoEditorDialog
from
"./MemoEditor/MemoEditorDialog"
;
import
showShareMemoDialog
from
"./ShareMemoDialog"
;
interface
Props
{
interface
Props
{
memo
:
Memo
;
memo
:
Memo
;
...
@@ -89,6 +89,11 @@ const MemoActionMenu = (props: Props) => {
...
@@ -89,6 +89,11 @@ const MemoActionMenu = (props: Props) => {
}
}
};
};
const
handleCopyLink
=
()
=>
{
copy
(
`
${
window
.
location
.
origin
}
/m/
${
memo
.
uid
}
`
);
toast
.
success
(
t
(
"message.succeed-copy-link"
));
};
const
handleDeleteMemoClick
=
async
()
=>
{
const
handleDeleteMemoClick
=
async
()
=>
{
showCommonDialog
({
showCommonDialog
({
title
:
t
(
"memo.delete-memo"
),
title
:
t
(
"memo.delete-memo"
),
...
@@ -126,9 +131,9 @@ const MemoActionMenu = (props: Props) => {
...
@@ -126,9 +131,9 @@ const MemoActionMenu = (props: Props) => {
</
MenuItem
>
</
MenuItem
>
)
}
)
}
{
!
hiddenActions
?.
includes
(
"share"
)
&&
(
{
!
hiddenActions
?.
includes
(
"share"
)
&&
(
<
MenuItem
onClick=
{
()
=>
showShareMemoDialog
(
extractMemoIdFromName
(
memo
.
name
))
}
>
<
MenuItem
onClick=
{
handleCopyLink
}
>
<
Icon
.
Share
className=
"w-4 h-auto"
/>
<
Icon
.
Copy
className=
"w-4 h-auto"
/>
{
t
(
"
common.share
"
)
}
{
t
(
"
memo.copy-link
"
)
}
</
MenuItem
>
</
MenuItem
>
)
}
)
}
<
MenuItem
color=
"warning"
onClick=
{
handleToggleMemoStatusClick
}
>
<
MenuItem
color=
"warning"
onClick=
{
handleToggleMemoStatusClick
}
>
...
...
web/src/components/ShareMemoDialog.tsx
deleted
100644 → 0
View file @
eda19839
import
{
Button
,
IconButton
,
Select
,
Option
}
from
"@mui/joy"
;
import
copy
from
"copy-to-clipboard"
;
import
React
,
{
useEffect
,
useRef
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
getDateTimeString
}
from
"@/helpers/datetime"
;
import
{
downloadFileFromUrl
}
from
"@/helpers/utils"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
toImage
from
"@/labs/html2image"
;
import
{
useUserStore
,
useMemoStore
,
MemoNamePrefix
}
from
"@/store/v1"
;
import
{
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
convertVisibilityToString
}
from
"@/utils/memo"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
Icon
from
"./Icon"
;
import
MemoContent
from
"./MemoContent"
;
import
MemoResourceListView
from
"./MemoResourceListView"
;
import
UserAvatar
from
"./UserAvatar"
;
import
VisibilityIcon
from
"./VisibilityIcon"
;
import
"@/less/share-memo-dialog.less"
;
interface
Props
extends
DialogProps
{
memoId
:
number
;
}
const
ShareMemoDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
memoId
,
destroy
}
=
props
;
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
userStore
=
useUserStore
();
const
memoStore
=
useMemoStore
();
const
downloadingImageState
=
useLoading
(
false
);
const
loadingState
=
useLoading
();
const
memoContainerRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
memo
=
memoStore
.
getMemoByName
(
`
${
MemoNamePrefix
}${
memoId
}
`
);
const
user
=
userStore
.
getUserByName
(
memo
.
creator
);
const
readonly
=
memo
?.
creator
!==
currentUser
?.
name
;
useEffect
(()
=>
{
(
async
()
=>
{
await
userStore
.
getOrFetchUserByName
(
memo
.
creator
);
loadingState
.
setFinish
();
})();
},
[]);
const
handleCloseBtnClick
=
()
=>
{
destroy
();
};
const
handleDownloadImageBtnClick
=
()
=>
{
if
(
!
memoContainerRef
.
current
)
{
return
;
}
downloadingImageState
.
setLoading
();
toImage
(
memoContainerRef
.
current
,
{
pixelRatio
:
window
.
devicePixelRatio
*
2
,
})
.
then
((
url
)
=>
{
downloadFileFromUrl
(
url
,
`memos-
${
getDateTimeString
(
Date
.
now
())}
.png`
);
downloadingImageState
.
setFinish
();
URL
.
revokeObjectURL
(
url
);
})
.
catch
((
err
)
=>
{
console
.
error
(
err
);
});
};
const
handleDownloadTextFileBtnClick
=
()
=>
{
const
blob
=
new
Blob
([
memo
.
content
],
{
type
:
"text/plain;charset=utf-8"
});
const
url
=
URL
.
createObjectURL
(
blob
);
downloadFileFromUrl
(
url
,
`memos-
${
getDateTimeString
(
Date
.
now
())}
.md`
);
URL
.
revokeObjectURL
(
url
);
};
const
handleCopyLinkBtnClick
=
()
=>
{
copy
(
`
${
window
.
location
.
origin
}
/m/
${
memo
.
uid
}
`
);
toast
.
success
(
t
(
"message.succeed-copy-link"
));
};
const
handleMemoVisibilityOptionChanged
=
async
(
visibility
:
Visibility
)
=>
{
const
updatedMemo
=
await
memoStore
.
updateMemo
(
{
name
:
memo
.
name
,
visibility
:
visibility
,
},
[
"visibility"
],
);
if
(
updatedMemo
.
visibility
==
visibility
)
{
toast
.
success
(
t
(
"message.update-succeed"
));
}
};
if
(
loadingState
.
isLoading
)
{
return
null
;
}
return
(
<>
<
div
className=
"dialog-header-container py-3 px-4 !mb-0 rounded-t-lg"
>
<
p
className=
""
>
{
t
(
"common.share"
)
}
Memo
</
p
>
<
IconButton
size=
"sm"
onClick=
{
handleCloseBtnClick
}
>
<
Icon
.
X
className=
"w-5 h-auto"
/>
</
IconButton
>
</
div
>
<
div
className=
"dialog-content-container w-full flex flex-col justify-start items-start relative"
>
<
div
className=
"px-4 pb-3 w-full flex flex-row justify-between items-center space-x-2"
>
<
div
className=
"flex flex-row justify-start items-center space-x-2"
>
<
Button
color=
"neutral"
variant=
"outlined"
disabled=
{
downloadingImageState
.
isLoading
}
onClick=
{
handleDownloadImageBtnClick
}
>
{
downloadingImageState
.
isLoading
?
(
<
Icon
.
Loader
className=
"w-4 h-auto mr-1 animate-spin"
/>
)
:
(
<
Icon
.
Download
className=
"w-4 h-auto mr-1"
/>
)
}
{
t
(
"common.image"
)
}
</
Button
>
<
Button
color=
"neutral"
variant=
"outlined"
onClick=
{
handleDownloadTextFileBtnClick
}
>
<
Icon
.
File
className=
"w-4 h-auto mr-1"
/>
{
t
(
"common.file"
)
}
</
Button
>
<
Button
color=
"neutral"
variant=
"outlined"
onClick=
{
handleCopyLinkBtnClick
}
>
<
Icon
.
Link
className=
"w-4 h-auto mr-1"
/>
{
t
(
"common.link"
)
}
</
Button
>
</
div
>
{
!
readonly
&&
(
<
Select
className=
"w-auto text-sm"
variant=
"plain"
value=
{
memo
.
visibility
}
startDecorator=
{
<
VisibilityIcon
visibility=
{
memo
.
visibility
}
/>
}
onChange=
{
(
_
,
visibility
)
=>
{
if
(
visibility
)
{
handleMemoVisibilityOptionChanged
(
visibility
);
}
}
}
>
{
[
Visibility
.
PRIVATE
,
Visibility
.
PROTECTED
,
Visibility
.
PUBLIC
].
map
((
item
)
=>
(
<
Option
key=
{
item
}
value=
{
item
}
className=
"whitespace-nowrap"
>
{
t
(
`memo.visibility.${convertVisibilityToString(item).toLowerCase()}`
as
any
)
}
</
Option
>
))
}
</
Select
>
)
}
</
div
>
<
div
className=
"w-full border-t dark:border-zinc-700 overflow-clip"
>
<
div
className=
"w-full h-auto select-none relative flex flex-col justify-start items-start bg-white dark:bg-zinc-800"
ref=
{
memoContainerRef
}
>
<
span
className=
"w-full px-6 pt-5 pb-2 text-sm text-gray-500"
>
{
getDateTimeString
(
memo
.
displayTime
)
}
</
span
>
<
div
className=
"w-full px-6 text-base pb-4 space-y-2"
>
<
MemoContent
memoName=
{
memo
.
name
}
nodes=
{
memo
.
nodes
}
readonly=
{
true
}
disableFilter
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
</
div
>
<
div
className=
"flex flex-row justify-between items-center w-full bg-gray-100 dark:bg-zinc-900 py-4 px-6"
>
<
div
className=
"flex flex-row justify-start items-center"
>
<
UserAvatar
className=
"mr-2"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
div
className=
"w-auto grow truncate flex mr-2 flex-col justify-center items-start"
>
<
span
className=
"w-full text truncate font-medium text-gray-600 dark:text-gray-300"
>
{
user
.
nickname
||
user
.
username
}
</
span
>
</
div
>
</
div
>
<
span
className=
"text-gray-500 dark:text-gray-400"
>
via memos
</
span
>
</
div
>
</
div
>
</
div
>
</
div
>
</>
);
};
export
default
function
showShareMemoDialog
(
memoId
:
number
):
void
{
generateDialog
(
{
className
:
"share-memo-dialog"
,
dialogName
:
"share-memo-dialog"
,
},
ShareMemoDialog
,
{
memoId
},
);
}
web/src/labs/html2image/convertResourceToDataURL.ts
deleted
100644 → 0
View file @
eda19839
const
cachedResourceMap
=
new
Map
<
string
,
string
>
();
const
convertResourceToDataURL
=
async
(
url
:
string
,
useCache
=
true
):
Promise
<
string
>
=>
{
if
(
useCache
&&
cachedResourceMap
.
has
(
url
))
{
return
Promise
.
resolve
(
cachedResourceMap
.
get
(
url
)
as
string
);
}
const
res
=
await
fetch
(
url
);
const
blob
=
await
res
.
blob
();
return
new
Promise
((
resolve
)
=>
{
const
reader
=
new
FileReader
();
reader
.
onloadend
=
()
=>
{
const
base64Url
=
reader
.
result
as
string
;
cachedResourceMap
.
set
(
url
,
base64Url
);
resolve
(
base64Url
);
};
reader
.
readAsDataURL
(
blob
);
});
};
export
default
convertResourceToDataURL
;
web/src/labs/html2image/getCloneStyledElement.ts
deleted
100644 → 0
View file @
eda19839
import
convertResourceToDataURL
from
"./convertResourceToDataURL"
;
const
applyStyles
=
async
(
sourceElement
:
HTMLElement
,
clonedElement
:
HTMLElement
)
=>
{
if
(
!
sourceElement
||
!
clonedElement
)
{
return
;
}
if
(
sourceElement
.
tagName
===
"IMG"
)
{
const
url
=
sourceElement
.
getAttribute
(
"src"
)
??
""
;
let
covertFailed
=
false
;
try
{
(
clonedElement
as
HTMLImageElement
).
src
=
await
convertResourceToDataURL
(
url
);
}
catch
(
error
)
{
covertFailed
=
true
;
}
if
(
covertFailed
)
{
throw
new
Error
(
`Failed to convert image to data URL:
${
url
}
`
);
}
}
const
sourceStyles
=
window
.
getComputedStyle
(
sourceElement
);
for
(
const
item
of
sourceStyles
)
{
clonedElement
.
style
.
setProperty
(
item
,
sourceStyles
.
getPropertyValue
(
item
),
sourceStyles
.
getPropertyPriority
(
item
));
}
for
(
let
i
=
0
;
i
<
clonedElement
.
childElementCount
;
i
++
)
{
await
applyStyles
(
sourceElement
.
children
[
i
]
as
HTMLElement
,
clonedElement
.
children
[
i
]
as
HTMLElement
);
}
};
const
getCloneStyledElement
=
async
(
element
:
HTMLElement
)
=>
{
const
clonedElementContainer
=
document
.
createElement
(
element
.
tagName
);
clonedElementContainer
.
innerHTML
=
element
.
innerHTML
;
await
applyStyles
(
element
,
clonedElementContainer
);
return
clonedElementContainer
;
};
export
default
getCloneStyledElement
;
web/src/labs/html2image/index.ts
deleted
100644 → 0
View file @
eda19839
/**
* HTML to Image
*
* References:
* 1. html-to-image: https://github.com/bubkoo/html-to-image
* 2. <foreignObject>: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject
*/
import
getCloneStyledElement
from
"./getCloneStyledElement"
;
import
waitImageLoaded
from
"./waitImageLoaded"
;
type
Options
=
Partial
<
{
backgroundColor
:
string
;
pixelRatio
:
number
;
}
>
;
const
getElementSize
=
(
element
:
HTMLElement
)
=>
{
const
{
width
,
height
}
=
window
.
getComputedStyle
(
element
);
return
{
width
:
parseInt
(
width
.
replace
(
"px"
,
""
)),
height
:
parseInt
(
height
.
replace
(
"px"
,
""
)),
};
};
const
convertSVGToDataURL
=
(
svg
:
SVGElement
):
string
=>
{
const
xml
=
new
XMLSerializer
().
serializeToString
(
svg
);
const
url
=
encodeURIComponent
(
xml
);
return
`data:image/svg+xml;charset=utf-8,
${
url
}
`
;
};
const
generateSVGElement
=
(
width
:
number
,
height
:
number
,
element
:
HTMLElement
):
SVGSVGElement
=>
{
const
xmlNS
=
"http://www.w3.org/2000/svg"
;
const
svgElement
=
document
.
createElementNS
(
xmlNS
,
"svg"
);
svgElement
.
setAttribute
(
"width"
,
`
${
width
}
`
);
svgElement
.
setAttribute
(
"height"
,
`
${
height
}
`
);
svgElement
.
setAttribute
(
"viewBox"
,
`0 0
${
width
}
${
height
}
`
);
const
foreignObject
=
document
.
createElementNS
(
xmlNS
,
"foreignObject"
);
foreignObject
.
setAttribute
(
"width"
,
"100%"
);
foreignObject
.
setAttribute
(
"height"
,
"100%"
);
foreignObject
.
setAttribute
(
"x"
,
"0"
);
foreignObject
.
setAttribute
(
"y"
,
"0"
);
foreignObject
.
setAttribute
(
"externalResourcesRequired"
,
"true"
);
foreignObject
.
appendChild
(
element
);
svgElement
.
appendChild
(
foreignObject
);
return
svgElement
;
};
export
const
toSVG
=
async
(
element
:
HTMLElement
,
options
?:
Options
)
=>
{
const
{
width
,
height
}
=
getElementSize
(
element
);
const
clonedElement
=
await
getCloneStyledElement
(
element
);
if
(
options
?.
backgroundColor
)
{
clonedElement
.
style
.
backgroundColor
=
options
.
backgroundColor
;
}
const
svg
=
generateSVGElement
(
width
,
height
,
clonedElement
);
const
url
=
convertSVGToDataURL
(
svg
);
return
url
;
};
export
const
toCanvas
=
async
(
element
:
HTMLElement
,
options
?:
Options
):
Promise
<
HTMLCanvasElement
>
=>
{
const
ratio
=
options
?.
pixelRatio
||
1
;
const
{
width
,
height
}
=
getElementSize
(
element
);
const
canvas
=
document
.
createElement
(
"canvas"
);
const
context
=
canvas
.
getContext
(
"2d"
);
if
(
!
context
)
{
return
Promise
.
reject
(
"Canvas error"
);
}
canvas
.
width
=
width
*
ratio
;
canvas
.
height
=
height
*
ratio
;
canvas
.
style
.
width
=
`
${
width
}
`
;
canvas
.
style
.
height
=
`
${
height
}
`
;
if
(
options
?.
backgroundColor
)
{
context
.
fillStyle
=
options
.
backgroundColor
;
context
.
fillRect
(
0
,
0
,
canvas
.
width
,
canvas
.
height
);
}
const
url
=
await
toSVG
(
element
,
options
);
const
imageEl
=
new
Image
();
imageEl
.
style
.
zIndex
=
"-1"
;
imageEl
.
style
.
position
=
"fixed"
;
imageEl
.
style
.
top
=
"0"
;
document
.
body
.
append
(
imageEl
);
await
waitImageLoaded
(
imageEl
,
url
);
context
.
drawImage
(
imageEl
,
0
,
0
,
canvas
.
width
,
canvas
.
height
);
imageEl
.
remove
();
return
canvas
;
};
const
toImage
=
async
(
element
:
HTMLElement
,
options
?:
Options
)
=>
{
const
canvas
=
await
toCanvas
(
element
,
options
);
return
canvas
.
toDataURL
();
};
export
default
toImage
;
web/src/labs/html2image/waitImageLoaded.ts
deleted
100644 → 0
View file @
eda19839
const
waitImageLoaded
=
(
image
:
HTMLImageElement
,
url
:
string
):
Promise
<
void
>
=>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
image
.
loading
=
"eager"
;
image
.
onload
=
()
=>
{
// NOTE: There is image loading problem in Safari, fix it with some trick
setTimeout
(()
=>
{
resolve
();
},
200
);
};
image
.
onerror
=
()
=>
{
reject
(
"Image load failed"
);
};
image
.
src
=
url
;
});
};
export
default
waitImageLoaded
;
web/src/less/share-memo-dialog.less
deleted
100644 → 0
View file @
eda19839
.share-memo-dialog {
> .dialog-container {
@apply w-112 max-w-full p-0 bg-white dark:bg-zinc-800;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment