feat: support to customize the filename

This commit is contained in:
Pionxzh
2023-01-12 02:23:41 +08:00
parent c8626c0894
commit 85b64b07de
9 changed files with 162 additions and 34 deletions

View File

@@ -18,6 +18,7 @@
"dependencies": {
"html2canvas": "^1.4.1",
"preact": "^10.11.3",
"sanitize-filename": "^1.6.3",
"sentinel-js": "^0.0.5",
"vite-plugin-monkey": "^2.10.0"
},

View File

@@ -1,11 +1,10 @@
import { ChatGPTAvatar } from '../icons'
import { getConversation } from '../parser'
import { timestamp } from '../utils/utils'
import { downloadFile } from '../utils/download'
import { downloadFile, getFileNameWithFormat } from '../utils/download'
import templateHtml from '../template.html?raw'
export function exportToHtml() {
export function exportToHtml(fileNameFormat: string) {
const conversations = getConversation()
if (conversations.length === 0) return alert('No conversation found. Please send a message first.')
@@ -65,7 +64,7 @@ ${linesHtml}
.replace('{{lang}}', lang)
.replace('{{content}}', conversationHtml)
const fileName = `ChatGPT-${timestamp()}.html`
const fileName = getFileNameWithFormat(fileNameFormat, 'html')
downloadFile(fileName, 'text/html', html)
}

View File

@@ -1,8 +1,8 @@
import html2canvas from 'html2canvas'
import { downloadUrl } from '../utils/download'
import { sleep, timestamp } from '../utils/utils'
import { downloadUrl, getFileNameWithFormat } from '../utils/download'
import { sleep } from '../utils/utils'
export async function exportToPng() {
export async function exportToPng(fileNameFormat: string) {
const thread = document.querySelector('main .group')?.parentElement as HTMLElement
if (!thread || thread.children.length === 0) return
@@ -23,6 +23,6 @@ export async function exportToPng() {
const dataUrl = canvas.toDataURL('image/png', 1)
.replace(/^data:image\/[^;]/, 'data:application/octet-stream')
const fileName = `ChatGPT-${timestamp()}.png`
const fileName = getFileNameWithFormat(fileNameFormat, 'png')
downloadUrl(fileName, dataUrl)
}

View File

@@ -1,15 +1,15 @@
import { getConversation } from '../parser'
import type { Conversation } from '../type'
import { downloadFile } from '../utils/download'
import { timestamp } from '../utils/utils'
import { downloadFile, getFileNameWithFormat } from '../utils/download'
import { lineToText } from './text'
export function exportToMarkdown() {
export function exportToMarkdown(fileNameFormat: string) {
const conversations = getConversation()
if (conversations.length === 0) return alert('No conversation found. Please send a message first.')
const text = conversationToMarkdown(conversations)
downloadFile(`chatgpt-${timestamp()}.md`, 'text/markdown', text)
const fileName = getFileNameWithFormat(fileNameFormat, 'md')
downloadFile(fileName, 'text/markdown', text)
}
function conversationToMarkdown(conversation: Conversation[]) {

View File

@@ -1,9 +1,12 @@
import type { FunctionComponent } from 'preact'
import type { JSXInternal } from 'preact/src/jsx'
import { exportToText } from './exporter/text'
import { exportToPng } from './exporter/image'
import { exportToMarkdown } from './exporter/markdown'
import { exportToHtml } from './exporter/html'
import { FileCode, IconArrowRightFromBracket, IconCamera, IconCopy, IconMarkdown } from './icons'
import { useGMStorage } from './useGMStorage'
import './style.css'
type FC<P = {}> = FunctionComponent<P>
@@ -32,7 +35,15 @@ const Dropdown: FC = ({ children }) => {
const Divider = () => <div className="border-b border-white/20"></div>
const KEY = 'exporter-format'
const defaultFormat = 'ChatGPT-{timestamp}'
export function Menu() {
const [format, setFormat] = useGMStorage(KEY, defaultFormat)
const handleChange: JSXInternal.GenericEventHandler<HTMLInputElement> = (e) => {
setFormat(e.currentTarget.value)
}
return (
<div id="exporter-menu" className="pt-1 relative">
<MenuItem>
@@ -40,19 +51,29 @@ export function Menu() {
Export
</MenuItem>
<Dropdown>
<fieldset className="inputFieldSet mb-2 rounded-md border-white/20 hover:bg-gray-500/10 duration-200">
<legend className="inputLabel px-2 text-xs">File Name: {'{title}, {timestamp}' }</legend>
<input
className="border-none text-sm w-full"
type="text"
onChange={handleChange}
value={format}
/>
</fieldset>
<MenuItem onClick={exportToText}>
<IconCopy />
Copy Text
</MenuItem>
<MenuItem onClick={exportToPng}>
<MenuItem onClick={() => exportToPng(format)}>
<IconCamera />
Screenshot
</MenuItem>
<MenuItem onClick={exportToMarkdown}>
<MenuItem onClick={() => exportToMarkdown(format)}>
<IconMarkdown />
Markdown
</MenuItem>
<MenuItem onClick={exportToHtml}>
<MenuItem onClick={() => exportToHtml(format)}>
<FileCode />
WebPage (HTML)
</MenuItem>

View File

@@ -1,9 +1,24 @@
.inputFieldSet {
display: block;
border-width: 2px;
border-style: groove;
}
.inputFieldSet legend {
margin-left: 4px;
}
.inputFieldSet input {
background-color: transparent;
box-shadow: none!important;
}
.dropdown-menu {
display: none;
position: absolute;
flex-direction: column;
left: calc(100% + 1rem);
top: 0;
top: -4.2rem;
width: 220px;
padding: .75rem .4rem 0 .4rem;
border-radius: .375rem;
@@ -19,11 +34,11 @@
height: 100%;
}
.dropdown-menu::after {
#exporter-menu:hover::after {
content: '';
position: absolute;
top: 1rem;
left: -0.5rem;
top: 1.2rem;
right: -1rem;
width: 0;
height: 0;
border-top: .5rem solid transparent;

View File

@@ -0,0 +1,56 @@
import { useState } from 'preact/hooks'
import { GM_getValue, GM_setValue } from 'vite-plugin-monkey/dist/client'
/**
* ref: https://usehooks.com/useLocalStorage/
* Hook to use GM storage with fallback to localStorage
*/
export function useGMStorage(key: string, initialValue: string): [string, (value: string) => void] {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
// Get from GM storage by key
return GM_getValue(key, initialValue)
}
catch (error) {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key)
// Parse stored json or if none return initialValue
return item ?? initialValue
}
catch (error) {
// If error also return initialValue
console.log(error)
return initialValue
}
}
})
// Return a wrapped version of useState's setter function that
// persists the new value to localStorage.
const setValue = (value: string) => {
// Save state
setStoredValue(value)
try {
// Save to GM storage
GM_setValue(key, value)
}
catch (error) {
try {
// Save to local storage
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(value))
}
}
catch (error) {
console.log(error)
}
}
}
return [storedValue, setValue]
}

View File

@@ -1,3 +1,6 @@
import sanitize from 'sanitize-filename'
import { timestamp } from './utils'
export function downloadFile(filename: string, type: string, content: string) {
const blob = new Blob([content], { type })
const url = URL.createObjectURL(blob)
@@ -17,3 +20,12 @@ export function downloadUrl(filename: string, url: string) {
a.click()
document.body.removeChild(a)
}
export function getFileNameWithFormat(format: string, ext: string) {
const title = sanitize(document.title).replace(/\s+/g, '_')
return format
.replace('{title}', title)
.replace('{timestamp}', timestamp())
.concat(`.${ext}`)
}

54
pnpm-lock.yaml generated
View File

@@ -38,16 +38,18 @@ importers:
'@preact/preset-vite': ^2.5.0
html2canvas: ^1.4.1
preact: ^10.11.3
sanitize-filename: ^1.6.3
sentinel-js: ^0.0.5
vite: ^4.0.3
vite-plugin-monkey: ^2.10.0
dependencies:
html2canvas: 1.4.1
preact: 10.11.3
sanitize-filename: 1.6.3
sentinel-js: 0.0.5_4xqsksf5w62wic46kncafprj3e
vite-plugin-monkey: 2.10.0_vite@4.0.3
devDependencies:
'@preact/preset-vite': 2.5.0_preact@10.11.3+vite@4.0.3
'@preact/preset-vite': 2.5.0_6nblbicqnlp2dxtgpm3busodtu
vite: 4.0.3
packages:
@@ -230,34 +232,37 @@ packages:
'@babel/types': 7.20.7
dev: true
/@babel/plugin-syntax-jsx/7.18.6:
/@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.20.12:
resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.20.12
'@babel/helper-plugin-utils': 7.20.2
dev: true
/@babel/plugin-transform-react-jsx-development/7.18.6:
/@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.20.12:
resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/plugin-transform-react-jsx': 7.20.7
'@babel/core': 7.20.12
'@babel/plugin-transform-react-jsx': 7.20.7_@babel+core@7.20.12
dev: true
/@babel/plugin-transform-react-jsx/7.20.7:
/@babel/plugin-transform-react-jsx/7.20.7_@babel+core@7.20.12:
resolution: {integrity: sha512-Tfq7qqD+tRj3EoDhY00nn2uP2hsRxgYGi5mLQ5TimKav0a9Lrpd4deE+fcLXU8zFYRjlKPHZhpCvfEA6qnBxqQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.20.12
'@babel/helper-annotate-as-pure': 7.18.6
'@babel/helper-module-imports': 7.18.6
'@babel/helper-plugin-utils': 7.20.2
'@babel/plugin-syntax-jsx': 7.18.6
'@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.12
'@babel/types': 7.20.7
dev: true
@@ -391,7 +396,7 @@ packages:
lodash.merge: 4.6.2
lodash.uniq: 4.5.0
resolve-from: 5.0.0
ts-node: 10.9.1_xplfzyzpegygk3axf4z63vz544
ts-node: 10.9.1_moeqx3xmzxqxagf2sz6mqkbb7m
typescript: 4.9.4
transitivePeerDependencies:
- '@swc/core'
@@ -803,17 +808,18 @@ packages:
- supports-color
dev: true
/@preact/preset-vite/2.5.0_preact@10.11.3+vite@4.0.3:
/@preact/preset-vite/2.5.0_6nblbicqnlp2dxtgpm3busodtu:
resolution: {integrity: sha512-BUhfB2xQ6ex0yPkrT1Z3LbfPzjpJecOZwQ/xJrXGFSZD84+ObyS//41RdEoQCMWsM0t7UHGaujUxUBub7WM1Jw==}
peerDependencies:
'@babel/core': 7.x
vite: 2.x || 3.x || 4.x
dependencies:
'@babel/plugin-transform-react-jsx': 7.20.7
'@babel/plugin-transform-react-jsx-development': 7.18.6
'@babel/core': 7.20.12
'@babel/plugin-transform-react-jsx': 7.20.7_@babel+core@7.20.12
'@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.20.12
'@prefresh/vite': 2.2.9_preact@10.11.3+vite@4.0.3
'@rollup/pluginutils': 4.2.1
babel-plugin-transform-hook-names: 1.0.2
babel-plugin-transform-hook-names: 1.0.2_@babel+core@7.20.12
debug: 4.3.4
kolorist: 1.6.0
resolve: 1.22.1
@@ -1231,10 +1237,12 @@ packages:
engines: {node: '>= 0.4'}
dev: true
/babel-plugin-transform-hook-names/1.0.2:
/babel-plugin-transform-hook-names/1.0.2_@babel+core@7.20.12:
resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==}
peerDependencies:
'@babel/core': ^7.12.10
dependencies:
'@babel/core': 7.20.12
dev: true
/balanced-match/1.0.2:
@@ -1498,7 +1506,7 @@ packages:
dependencies:
'@types/node': 14.18.35
cosmiconfig: 7.1.0
ts-node: 10.9.1_xplfzyzpegygk3axf4z63vz544
ts-node: 10.9.1_moeqx3xmzxqxagf2sz6mqkbb7m
typescript: 4.9.4
dev: true
@@ -3769,6 +3777,12 @@ packages:
regexp-tree: 0.1.24
dev: true
/sanitize-filename/1.6.3:
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
dependencies:
truncate-utf8-bytes: 1.0.2
dev: false
/semver/5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
hasBin: true
@@ -4061,7 +4075,13 @@ packages:
engines: {node: '>=12'}
dev: true
/ts-node/10.9.1_xplfzyzpegygk3axf4z63vz544:
/truncate-utf8-bytes/1.0.2:
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
dependencies:
utf8-byte-length: 1.0.4
dev: false
/ts-node/10.9.1_moeqx3xmzxqxagf2sz6mqkbb7m:
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
@@ -4080,7 +4100,7 @@ packages:
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.3
'@types/node': 14.18.35
'@types/node': 18.11.17
acorn: 8.8.0
acorn-walk: 8.2.0
arg: 4.1.3
@@ -4268,6 +4288,10 @@ packages:
punycode: 2.1.1
dev: true
/utf8-byte-length/1.0.4:
resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==}
dev: false
/util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true