mirror of
https://github.com/pionxzh/chatgpt-exporter.git
synced 2026-07-23 09:00:51 -05:00
feat: support to customize the filename
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
56
packages/userscript/src/useGMStorage.ts
Normal file
56
packages/userscript/src/useGMStorage.ts
Normal 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]
|
||||
}
|
||||
@@ -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
54
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user