feat: add conversation timestamp on page and HTML, revamp setting UI

closes #54, closes #104
This commit is contained in:
Pionxzh
2023-04-08 01:51:45 +08:00
parent 41fd500863
commit 531a766bc8
15 changed files with 450 additions and 225 deletions

View File

@@ -2,5 +2,7 @@ export const baseUrl = 'https://chat.openai.com'
export const LEGACY_KEY_FILENAME_FORMAT = 'exporter-format'
export const KEY_FILENAME_FORMAT = 'exporter:filename_format'
export const KEY_TIMESTAMP_ENABLED = 'exporter:enable_timestamp'
export const KEY_TIMESTAMP_24H = 'exporter:timestamp_24h'
export const KEY_META_ENABLED = 'exporter:enable_meta'
export const KEY_META_LIST = 'exporter:meta_list'

View File

@@ -1,14 +1,15 @@
import JSZip from 'jszip'
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
import { baseUrl } from '../constants'
import { KEY_TIMESTAMP_24H, KEY_TIMESTAMP_ENABLED, baseUrl } from '../constants'
import { checkIfConversationStarted, getConversationChoice, getUserAvatar } from '../page'
import templateHtml from '../template.html?raw'
import { downloadFile, getFileNameWithFormat } from '../utils/download'
import { fromMarkdown, toHtml } from '../utils/markdown'
import { ScriptStorage } from '../utils/storage'
import { standardizeLineBreaks } from '../utils/text'
import { dateStr, getColorScheme, timestamp } from '../utils/utils'
import type { ApiConversationWithId, ConversationResult } from '../api'
import type { ExportMeta } from '../ui/MetaContext'
import type { ExportMeta } from '../ui/SettingContext'
export async function exportToHtml(fileNameFormat: string, metaList: ExportMeta[]) {
if (!checkIfConversationStarted()) {
@@ -71,17 +72,22 @@ function conversationToHtml(conversation: ConversationResult, avatar: string, me
conversationContent = `<p>${escapeHtml(content)}</p>`
}
const enableTimestamp = ScriptStorage.get<boolean>(KEY_TIMESTAMP_ENABLED) ?? false
const timeStamp24H = ScriptStorage.get<boolean>(KEY_TIMESTAMP_24H) ?? false
const timestamp = item.message?.create_time ?? ''
const showTimestamp = enableTimestamp && timestamp
let conversationDate = ''
let conversationTime = ''
if (timestamp) {
if (showTimestamp) {
const date = new Date(timestamp * 1000)
const isoStr = date.toISOString()
// format: 2022-01-01 10:12:00 UTC
conversationDate = `${isoStr.split('T')[0]} ${isoStr.split('T')[1].split('.')[0]} UTC`
// format: 10:12 AM
conversationTime = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
// format: 20:12 / 08:12 PM
conversationTime = timeStamp24H
? date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })
: date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
}
return `
@@ -94,7 +100,7 @@ function conversationToHtml(conversation: ConversationResult, avatar: string, me
${conversationContent}
</div>
</div>
${timestamp ? `<div class="time" title="${conversationDate}">${conversationTime}</div>` : ''}
${showTimestamp ? `<div class="time" title="${conversationDate}">${conversationTime}</div>` : ''}
</div>`
}).join('\n\n')

View File

@@ -7,7 +7,7 @@ import { fromMarkdown, toMarkdown } from '../utils/markdown'
import { standardizeLineBreaks } from '../utils/text'
import { dateStr, timestamp } from '../utils/utils'
import type { ApiConversationWithId, ConversationResult } from '../api'
import type { ExportMeta } from '../ui/MetaContext'
import type { ExportMeta } from '../ui/SettingContext'
export async function exportToMarkdown(fileNameFormat: string, metaList: ExportMeta[]) {
if (!checkIfConversationStarted()) {

View File

@@ -1,7 +1,9 @@
import { render } from 'preact'
import sentinel from 'sentinel-js'
import { GM_deleteValue, GM_getValue, GM_setValue } from 'vite-plugin-monkey/dist/client'
import { fetchConversation, processConversation } from './api'
import { KEY_FILENAME_FORMAT, LEGACY_KEY_FILENAME_FORMAT } from './constants'
import { getChatIdFromUrl, getConversationChoice } from './page'
import { Menu } from './ui/Menu'
import { SecondaryToolbar } from './ui/SecondaryToolbar'
import { onloadSafe } from './utils/utils'
@@ -67,7 +69,7 @@ function main() {
})
/** Insert copy button to the next of feedback buttons */
sentinel.on('.flex.justify-between', (node) => {
sentinel.on('main .flex.justify-between', (node) => {
if (!node.querySelector('button')) return
// ignore codeblock
if (node.closest('pre')) return
@@ -79,5 +81,40 @@ function main() {
render(<SecondaryToolbar index={index} />, secondaryToolbar)
node.append(secondaryToolbar)
})
/** Insert timestamp to the bottom right of each message */
let chatId = ''
sentinel.on('main .group', async () => {
const threadContents = Array.from(document.querySelectorAll('main .group > .text-base > .relative:nth-child(2)'))
const currentChatId = getChatIdFromUrl()
if (!currentChatId || currentChatId === chatId) return
chatId = currentChatId
const rawConversation = await fetchConversation(chatId)
const conversationChoices = getConversationChoice()
const { conversationNodes } = processConversation(rawConversation, conversationChoices)
threadContents.forEach((thread, index) => {
const createTime = conversationNodes[index].message?.create_time
if (!createTime) return
const date = new Date(createTime * 1000)
const timestamp = document.createElement('time')
timestamp.className = 'text-gray-500 dark:text-gray-400 text-sm text-right'
timestamp.dateTime = date.toISOString()
timestamp.title = date.toLocaleString()
const hour12 = document.createElement('span')
hour12.setAttribute('data-time-format', '12')
hour12.textContent = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
const hour24 = document.createElement('span')
hour24.setAttribute('data-time-format', '24')
hour24.textContent = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })
timestamp.append(hour12, hour24)
thread.append(timestamp)
})
})
})
}

View File

@@ -7,6 +7,18 @@ p > img[src*="https://images.unsplash.com/"] {
animation: fadeIn .3s;
}
span[data-time-format] {
display: none;
}
body[data-time-format="12"] span[data-time-format="12"] {
display: inline;
}
body[data-time-format="24"] span[data-time-format="24"] {
display: inline;
}
.Select {
padding: 0 0 0 0.5rem;
width: 7.5rem;

View File

@@ -1,3 +1,62 @@
.after\:content-\[\'\'\]:after {
--tw-content: "";
content: var(--tw-content);
}
.after\:transition-all:after {
content: var(--tw-content);
transition-duration: .15s;
transition-property: all;
transition-timing-function: cubic-bezier(.4,0,.2,1);
}
.after\:bg-white:after {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255/var(--tw-bg-opacity));
content: var(--tw-content);
}
.after\:border-gray-300:after {
--tw-border-opacity: 1;
border-color: rgb(209 213 219/var(--tw-border-opacity));
content: var(--tw-content);
}
.after\:border:after {
border-width: 1px;
content: var(--tw-content);
}
.after\:rounded-full:after {
border-radius: 9999px;
content: var(--tw-content);
}
.after\:w-4:after {
content: var(--tw-content);
width: 1rem
}
.after\:h-4:after {
content: var(--tw-content);
height: 1rem;
}
.after\:top-\[2px\]:after {
content: var(--tw-content);
top: 2px;
}
.after\:left-\[2px\]:after {
content: var(--tw-content);
left: 2px;
}
.after\:absolute:after {
content: var(--tw-content);
position: absolute;
}
.animate-fadeIn {
animation: fadeIn .3s;
}
@@ -6,11 +65,6 @@
animation: slideUp .3s;
}
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235/var(--tw-bg-opacity));
}
.bg-blue-600 {
--tw-bg-opacity: 1;
background-color: rgb(28 100 242/var(--tw-bg-opacity));
@@ -24,13 +78,22 @@
cursor: help;
}
.dark .dark\:bg-white\/5 {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / 5%);
}
.dark .dark\:border-gray-\[\#86858d\] {
border-color: #86858d;
}
.dark .dark\:hover\:bg-gray-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81/var(--tw-bg-opacity));
.dark .dark\:border-gray-600 {
--tw-border-opacity: 1;
border-color: rgb(75 85 99/var(--tw-border-opacity));
}
.empty\:hidden:empty {
display: none;
}
.fill-current {
@@ -45,6 +108,14 @@
height: 0.625rem;
}
.h-4 {
height: 1rem;
}
.h-5 {
height: 1.25rem;
}
.mr-8 {
margin-right: 2rem;
}
@@ -53,6 +124,26 @@
padding-bottom: 0;
}
.peer:checked~.peer-checked\:bg-blue-600 {
--tw-bg-opacity: 1;
background-color: rgb(28 100 242/var(--tw-bg-opacity));
}
.peer:checked~.peer-checked\:after\:border-white:after {
--tw-border-opacity: 1;
border-color: rgb(255 255 255/var(--tw-border-opacity));
content: var(--tw-content);
}
.peer:checked~.peer-checked\:after\:translate-x-full:after {
--tw-translate-x: 100%;
content: var(--tw-content);
transform: translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.pl-4 {
padding-left: 1rem;
}
.pr-8 {
padding-right: 2rem;
}
@@ -77,6 +168,10 @@
white-space: nowrap;
}
.w-9 {
width: 2.25rem;
}
.whitespace-nowrap {
white-space: nowrap;
}

View File

@@ -7,7 +7,7 @@
}
.DialogContent {
background-color: white;
background-color: #f3f3f3;
border-radius: 6px;
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
position: fixed;
@@ -15,7 +15,7 @@
left: 50%;
transform: translate(-50%, -50%);
width: 90vw;
max-width: 450px;
max-width: 560px;
max-height: 85vh;
overflow-x: hidden;
overflow-y: auto;

View File

@@ -7,7 +7,7 @@ import { exportAllToMarkdown } from '../exporter/markdown'
import { RequestQueue } from '../utils/queue'
import { CheckBox } from './CheckBox'
import { IconCross } from './Icons'
import { useMetaDataContext } from './MetaContext'
import { useSettingContext } from './SettingContext'
import type { ApiConversationItem, ApiConversationWithId } from '../api'
import type { FC } from '../type'
@@ -76,7 +76,7 @@ interface ExportDialogProps {
}
export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange, children }) => {
const { enableMeta, exportMetaList } = useMetaDataContext()
const { enableMeta, exportMetaList } = useSettingContext()
const metaList = useMemo(() => enableMeta ? exportMetaList : [], [enableMeta, exportMetaList])
const [conversations, setConversations] = useState<ApiConversationItem[]>([])

View File

@@ -1,25 +0,0 @@
import { createContext, useContext } from 'preact/compat'
import { KEY_FILENAME_FORMAT } from '../constants'
import { useGMStorage } from '../hooks/useGMStorage'
import type { FC } from 'preact/compat'
const defaultFormat = 'ChatGPT-{title}'
const FormatContext = createContext({
format: defaultFormat,
setFormat: (_: string) => {},
})
export const FormatProvider: FC = ({ children }) => {
const [format, setFormat] = useGMStorage(KEY_FILENAME_FORMAT, defaultFormat)
return (
<FormatContext.Provider
value={{ format, setFormat }}
>
{children}
</FormatContext.Provider>
)
}
export const useFormatContext = () => useContext(FormatContext)

View File

@@ -1,5 +1,5 @@
import * as HoverCard from '@radix-ui/react-hover-card'
import { useCallback, useMemo, useState } from 'preact/hooks'
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
import { exportToHtml } from '../exporter/html'
import { exportToPng } from '../exporter/image'
import { exportToJson } from '../exporter/json'
@@ -8,10 +8,9 @@ import { exportToText } from '../exporter/text'
import { getHistoryDisabled } from '../page'
import { Divider } from './Divider'
import { ExportDialog } from './ExportDialog'
import { FormatProvider, useFormatContext } from './FormatContext'
import { FileCode, IconArrowRightFromBracket, IconCamera, IconCopy, IconJSON, IconMarkdown, IconSetting, IconZip } from './Icons'
import { MenuItem } from './MenuItem'
import { MetaDataProvider, useMetaDataContext } from './MetaContext'
import { SettingProvider, useSettingContext } from './SettingContext'
import { SettingDialog } from './SettingDialog'
import '../style.css'
@@ -28,8 +27,24 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
const [exportOpen, setExportOpen] = useState(false)
const [settingOpen, setSettingOpen] = useState(false)
const { format } = useFormatContext()
const { enableMeta, exportMetaList } = useMetaDataContext()
const {
format,
enableTimestamp,
timeStamp24H,
enableMeta,
exportMetaList,
} = useSettingContext()
useEffect(() => {
console.log('enableTimestamp', enableTimestamp, 'timeStamp24H', timeStamp24H)
if (enableTimestamp) {
document.body.setAttribute('data-time-format', timeStamp24H ? '24' : '12')
}
else {
document.body.removeAttribute('data-time-format')
}
}, [enableTimestamp, timeStamp24H])
const metaList = useMemo(() => enableMeta ? exportMetaList : [], [enableMeta, exportMetaList])
const onClickText = useCallback(() => exportToText(), [])
@@ -161,10 +176,8 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
export function Menu({ container }: { container: HTMLDivElement }) {
return (
<FormatProvider>
<MetaDataProvider>
<MenuInner container={container} />
</MetaDataProvider>
</FormatProvider>
<SettingProvider>
<MenuInner container={container} />
</SettingProvider>
)
}

View File

@@ -1,41 +0,0 @@
import { createContext, useContext } from 'preact/compat'
import { KEY_META_ENABLED, KEY_META_LIST } from '../constants'
import { useGMStorage } from '../hooks/useGMStorage'
import type { FC } from 'preact/compat'
export interface ExportMeta {
name: string
value: string
}
const defaultExportMetaList: ExportMeta[] = [
{ name: 'title', value: '{title}' },
{ name: 'source', value: '{source}' },
]
const MetaContext = createContext({
enableMeta: false,
setEnableMeta: (_: boolean) => {},
exportMetaList: defaultExportMetaList,
setExportMetaList: (_: ExportMeta[]) => {},
})
export const MetaDataProvider: FC = ({ children }) => {
const [enableMeta, setEnableMeta] = useGMStorage(KEY_META_ENABLED, false)
const [exportMetaList, setExportMetaList] = useGMStorage(KEY_META_LIST, defaultExportMetaList)
return (
<MetaContext.Provider
value={{
enableMeta,
setEnableMeta,
exportMetaList,
setExportMetaList,
}}
>
{children}
</MetaContext.Provider>
)
}
export const useMetaDataContext = () => useContext(MetaContext)

View File

@@ -0,0 +1,76 @@
import { createContext, useContext } from 'preact/compat'
import { useCallback } from 'preact/hooks'
import { KEY_FILENAME_FORMAT, KEY_META_ENABLED, KEY_META_LIST, KEY_TIMESTAMP_24H, KEY_TIMESTAMP_ENABLED } from '../constants'
import { useGMStorage } from '../hooks/useGMStorage'
import type { FC } from 'preact/compat'
const defaultFormat = 'ChatGPT-{title}'
export interface ExportMeta {
name: string
value: string
}
const defaultExportMetaList: ExportMeta[] = [
{ name: 'title', value: '{title}' },
{ name: 'source', value: '{source}' },
]
const SettingContext = createContext({
format: defaultFormat,
setFormat: (_: string) => {},
enableTimestamp: false,
setEnableTimestamp: (_: boolean) => {},
timeStamp24H: false,
setTimeStamp24H: (_: boolean) => {},
enableMeta: false,
setEnableMeta: (_: boolean) => {},
exportMetaList: defaultExportMetaList,
setExportMetaList: (_: ExportMeta[]) => {},
resetDefault: () => {},
})
export const SettingProvider: FC = ({ children }) => {
const [format, setFormat] = useGMStorage(KEY_FILENAME_FORMAT, defaultFormat)
const [enableTimestamp, setEnableTimestamp] = useGMStorage(KEY_TIMESTAMP_ENABLED, false)
const [timeStamp24H, setTimeStamp24H] = useGMStorage(KEY_TIMESTAMP_24H, false)
const [enableMeta, setEnableMeta] = useGMStorage(KEY_META_ENABLED, false)
const [exportMetaList, setExportMetaList] = useGMStorage(KEY_META_LIST, defaultExportMetaList)
const resetDefault = useCallback(() => {
setFormat(defaultFormat)
setEnableMeta(false)
setExportMetaList(defaultExportMetaList)
}, [setFormat, setEnableMeta, setExportMetaList])
return (
<SettingContext.Provider
value={{
format,
setFormat,
enableTimestamp,
setEnableTimestamp,
timeStamp24H,
setTimeStamp24H,
enableMeta,
setEnableMeta,
exportMetaList,
setExportMetaList,
resetDefault,
}}
>
{children}
</SettingContext.Provider>
)
}
export const useSettingContext = () => useContext(SettingContext)

View File

@@ -5,10 +5,9 @@ import { useTitle } from '../hooks/useTitle'
import { getChatIdFromUrl } from '../page'
import { getFileNameWithFormat } from '../utils/download'
import { timestamp as _timestamp, dateStr } from '../utils/utils'
import { CheckBox } from './CheckBox'
import { useFormatContext } from './FormatContext'
import { IconCross, IconTrash } from './Icons'
import { useMetaDataContext } from './MetaContext'
import { useSettingContext } from './SettingContext'
import { Toggle } from './Toggle'
import type { FC } from '../type'
const Variable = ({ name, title }: { name: string; title: string }) => (
@@ -25,9 +24,13 @@ export const SettingDialog: FC<SettingDialogProps> = ({
onOpenChange,
children,
}) => {
const { format, setFormat } = useFormatContext()
const { enableMeta, setEnableMeta, exportMetaList, setExportMetaList } = useMetaDataContext()
const {
format, setFormat,
enableTimestamp, setEnableTimestamp,
timeStamp24H, setTimeStamp24H,
enableMeta, setEnableMeta,
exportMetaList, setExportMetaList,
} = useSettingContext()
const _title = useTitle()
const date = dateStr()
const timestamp = _timestamp()
@@ -51,101 +54,122 @@ export const SettingDialog: FC<SettingDialogProps> = ({
<Dialog.Title className="DialogTitle">Exporter Setting</Dialog.Title>
<dl className="space-y-6">
<div>
<dt>
<label className="text-sm font-medium text-gray-800 dark:text-white" htmlFor="filename">
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
<div>
<dt className="text-md font-medium text-gray-800 dark:text-white">
File Name
</label>
</dt>
<dd>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-300">
Available variables:&nbsp;
<Variable name="{title}" title={title} />
,&nbsp;
<Variable name="{date}" title={date} />
,&nbsp;
<Variable name="{timestamp}" title={timestamp} />
,&nbsp;
<Variable name="{chat_id}" title={chatId} />
</p>
<input className="Input mt-1" id="filename" value={format} onChange={e => setFormat(e.currentTarget.value)} />
<p className="mt-1 text-sm text-gray-700 dark:text-gray-300">
Preview:&nbsp;
<span className="select-all" style={{ 'text-decoration': 'underline', 'text-underline-offset': 4 }}>{preview}</span>
</p>
</dd>
</dt>
<dd>
<p className="text-sm text-gray-700 dark:text-gray-300">
Available variables:{' '}
<Variable name="{title}" title={title} />
,{' '}
<Variable name="{date}" title={date} />
,{' '}
<Variable name="{timestamp}" title={timestamp} />
,{' '}
<Variable name="{chat_id}" title={chatId} />
</p>
<input className="Input mt-4" id="filename" value={format} onChange={e => setFormat(e.currentTarget.value)} />
<p className="mt-1 text-sm text-gray-700 dark:text-gray-300">
Preview:{' '}
<span className="select-all" style={{ 'text-decoration': 'underline', 'text-underline-offset': 4 }}>{preview}</span>
</p>
</dd>
</div>
</div>
<div>
<dt>
<div className="text-sm font-medium text-gray-800 dark:text-white">
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
<div>
<dt className="text-md font-medium text-gray-800 dark:text-white">
Conversation Timestamp
</dt>
<dd className="text-sm text-gray-700 dark:text-gray-300">
Will show on the page and HTML files.
{enableTimestamp && (
<div className="mt-2">
<Toggle
label="Use 24-hour format (eg. 23:59)"
checked={timeStamp24H}
onCheckedUpdate={setTimeStamp24H}
/>
</div>
)}
</dd>
</div>
<div className="absolute right-4">
<Toggle label="" checked={enableTimestamp} onCheckedUpdate={setEnableTimestamp} />
</div>
</div>
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
<div>
<dt className="text-md font-medium text-gray-800 dark:text-white">
Export Metadata
</div>
<p className="mt-1 text-sm text-gray-700 dark:text-gray-300">
</dt>
<dd className="text-sm text-gray-700 dark:text-gray-300">
Add metadata to exported Markdown and HTML files.
</p>
</dt>
<dd>
<CheckBox label="Enable" className="mt-2" checked={enableMeta} onCheckedChange={setEnableMeta} />
{enableMeta && (
<>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-300">
Available variables:&nbsp;
<Variable name="{title}" title={title} />
,&nbsp;
<Variable name="{date}" title={date} />
,&nbsp;
<Variable name="{timestamp}" title={timestamp} />
,&nbsp;
<Variable name="{source}" title={source} />
,&nbsp;
<br />
<Variable name="{model}" title={'ChatGPT-3.5'} />
,&nbsp;
<Variable name="{model_name}" title={'text-davinci-002-render-sha'} />
</p>
{exportMetaList.map((meta, i) => (
<div className="flex items-center mt-2" key={i}>
<input
className="Input"
value={meta.name}
onChange={(e) => {
const list = [...exportMetaList]
list[i] = { ...list[i], name: e.currentTarget.value }
setExportMetaList(list)
}}
/>
<span className="mx-2"></span>
<input
className="Input"
value={meta.value}
onChange={(e) => {
const list = [...exportMetaList]
list[i] = { ...list[i], value: e.currentTarget.value }
setExportMetaList(list)
}}
/>
{enableMeta && (
<>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-300">
Available variables:{' '}
<Variable name="{title}" title={title} />
,{' '}
<Variable name="{date}" title={date} />
,{' '}
<Variable name="{timestamp}" title={timestamp} />
,{' '}
<Variable name="{source}" title={source} />
,{' '}
<Variable name="{model}" title={'ChatGPT-3.5'} />
,{' '}
<Variable name="{model_name}" title={'text-davinci-002-render-sha'} />
</p>
{exportMetaList.map((meta, i) => (
<div className="flex items-center mt-2" key={i}>
<input
className="Input"
value={meta.name}
onChange={(e) => {
const list = [...exportMetaList]
list[i] = { ...list[i], name: e.currentTarget.value }
setExportMetaList(list)
}}
/>
<span className="mx-2"></span>
<input
className="Input"
value={meta.value}
onChange={(e) => {
const list = [...exportMetaList]
list[i] = { ...list[i], value: e.currentTarget.value }
setExportMetaList(list)
}}
/>
<button
className="ml-2 rounded-full p-1 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition ease-in-out duration-150"
aria-label="Remove"
onClick={() => setExportMetaList(exportMetaList.filter((_, j) => j !== i))}
>
<IconTrash className="w-4 h-4" />
</button>
</div>
))}
<div className="flex justify-center items-center mt-2 pr-8">
<button
className="ml-2 rounded-full p-1 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition ease-in-out duration-150"
aria-label="Remove"
onClick={() => setExportMetaList(exportMetaList.filter((_, j) => j !== i))}
className="w-full border border-[#6f6e77] dark:border-gray-[#86858d] rounded-md py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition ease-in-out duration-150"
aria-label="Add"
onClick={() => setExportMetaList([...exportMetaList, { name: '', value: '' }])}
>
<IconTrash className="w-4 h-4" />
+
</button>
</div>
))}
<div className="flex justify-center items-center mt-2 pr-8">
<button
className="w-full border border-[#6f6e77] dark:border-gray-[#86858d] rounded-md py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition ease-in-out duration-150"
aria-label="Add"
onClick={() => setExportMetaList([...exportMetaList, { name: '', value: '' }])}
>
+
</button>
</div>
</>
)}
</dd>
</>
)}
</dd>
</div>
<div className="absolute right-4">
<Toggle label="" checked={enableMeta} onCheckedUpdate={setEnableMeta} />
</div>
</div>
</dl>
<div className="flex mt-6" style={{ justifyContent: 'flex-end' }}>

View File

@@ -0,0 +1,25 @@
interface ToggleProps {
label?: string
checked?: boolean
onCheckedUpdate?: (checked: boolean) => void
}
export const Toggle = (props: ToggleProps) => {
const { label = '', checked = true, onCheckedUpdate } = props
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target || !onCheckedUpdate) return
onCheckedUpdate((e.target as HTMLInputElement).checked)
}
return (
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={checked}
onChange={handleChange}
className="sr-only peer"
/>
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300 empty:hidden">{label}</span>
</label>
)
}

75
pnpm-lock.yaml generated
View File

@@ -28,7 +28,7 @@ importers:
cpy-cli: 4.2.0
eslint: 8.37.0
husky: 8.0.3
lint-staged: 13.2.0
lint-staged: 13.2.1
rimraf: 4.4.1
turbo: 1.8.8
typescript: 5.0.3
@@ -86,12 +86,12 @@ importers:
packages:
/@ampproject/remapping/2.2.0:
resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==}
/@ampproject/remapping/2.2.1:
resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/gen-mapping': 0.1.1
'@jridgewell/trace-mapping': 0.3.17
'@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.18
dev: true
/@babel/code-frame/7.21.4:
@@ -110,7 +110,7 @@ packages:
resolution: {integrity: sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==}
engines: {node: '>=6.9.0'}
dependencies:
'@ampproject/remapping': 2.2.0
'@ampproject/remapping': 2.2.1
'@babel/code-frame': 7.21.4
'@babel/generator': 7.21.4
'@babel/helper-compilation-targets': 7.21.4_@babel+core@7.21.4
@@ -134,8 +134,8 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.21.4
'@jridgewell/gen-mapping': 0.3.2
'@jridgewell/trace-mapping': 0.3.17
'@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.18
jsesc: 2.5.2
dev: true
@@ -774,21 +774,13 @@ packages:
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true
/@jridgewell/gen-mapping/0.1.1:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
/@jridgewell/gen-mapping/0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/set-array': 1.1.2
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@jridgewell/gen-mapping/0.3.2:
resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/set-array': 1.1.2
'@jridgewell/sourcemap-codec': 1.4.14
'@jridgewell/trace-mapping': 0.3.17
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.18
dev: true
/@jridgewell/resolve-uri/3.1.0:
@@ -796,6 +788,11 @@ packages:
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/resolve-uri/3.1.1:
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/set-array/1.1.2:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
@@ -805,8 +802,12 @@ packages:
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
dev: true
/@jridgewell/trace-mapping/0.3.17:
resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
/@jridgewell/sourcemap-codec/1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
dev: true
/@jridgewell/trace-mapping/0.3.18:
resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==}
dependencies:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
@@ -815,8 +816,8 @@ packages:
/@jridgewell/trace-mapping/0.3.9:
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
dependencies:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
'@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/@nodelib/fs.scandir/2.1.5:
@@ -918,7 +919,7 @@ packages:
babel-plugin-transform-hook-names: 1.0.2_@babel+core@7.21.4
debug: 4.3.4
kolorist: 1.7.0
resolve: 1.22.1
resolve: 1.22.2
vite: 4.2.1
transitivePeerDependencies:
- preact
@@ -1673,7 +1674,7 @@ packages:
hasBin: true
dependencies:
caniuse-lite: 1.0.30001474
electron-to-chromium: 1.4.352
electron-to-chromium: 1.4.355
node-releases: 2.0.10
update-browserslist-db: 1.0.10_browserslist@4.21.5
dev: true
@@ -2151,8 +2152,8 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
/electron-to-chromium/1.4.352:
resolution: {integrity: sha512-ikFUEyu5/q+wJpMOxWxTaEVk2M1qKqTGKKyfJmod1CPZxKfYnxVS41/GCBQg21ItBpZybyN8sNpRqCUGm+Zc4Q==}
/electron-to-chromium/1.4.355:
resolution: {integrity: sha512-056hxzEE4l667YeOccgjhRr5fTiwZ6EIJ4FpzGps4k3YcS8iAhiaBYUBrv5E2LDQJsussscv9EEUwAYKnv+ZKg==}
dev: true
/emoji-regex/8.0.0:
@@ -2290,7 +2291,7 @@ packages:
dependencies:
debug: 3.2.7
is-core-module: 2.11.0
resolve: 1.22.1
resolve: 1.22.2
transitivePeerDependencies:
- supports-color
dev: true
@@ -2376,7 +2377,7 @@ packages:
is-glob: 4.0.3
minimatch: 3.1.2
object.values: 1.1.6
resolve: 1.22.1
resolve: 1.22.2
semver: 6.3.0
tsconfig-paths: 3.14.2
transitivePeerDependencies:
@@ -2443,7 +2444,7 @@ packages:
ignore: 5.2.4
is-core-module: 2.11.0
minimatch: 3.1.2
resolve: 1.22.1
resolve: 1.22.2
semver: 7.3.8
dev: true
@@ -3596,8 +3597,8 @@ packages:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
/lint-staged/13.2.0:
resolution: {integrity: sha512-GbyK5iWinax5Dfw5obm2g2ccUiZXNGtAS4mCbJ0Lv4rq6iEtfBSjOYdcbOtAIFtM114t0vdpViDDetjVTSd8Vw==}
/lint-staged/13.2.1:
resolution: {integrity: sha512-8gfzinVXoPfga5Dz/ZOn8I2GOhf81Wvs+KwbEXQn/oWZAvCVS2PivrXfVbFJc93zD16uC0neS47RXHIjXKYZQw==}
engines: {node: ^14.13.1 || >=16.0.0}
hasBin: true
dependencies:
@@ -4326,7 +4327,7 @@ packages:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies:
hosted-git-info: 2.8.9
resolve: 1.22.1
resolve: 1.22.2
semver: 5.7.1
validate-npm-package-license: 3.0.4
dev: true
@@ -4880,8 +4881,8 @@ packages:
global-dirs: 0.1.1
dev: true
/resolve/1.22.1:
resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
/resolve/1.22.2:
resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==}
hasBin: true
dependencies:
is-core-module: 2.11.0
@@ -5704,7 +5705,7 @@ packages:
dependencies:
esbuild: 0.17.15
postcss: 8.4.21
resolve: 1.22.1
resolve: 1.22.2
rollup: 3.20.2
optionalDependencies:
fsevents: 2.3.2