mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(export): 多会话导出布局选择与无媒体直出
- 多会话媒体导出支持共享/分会话目录 - 无媒体导出时直接输出到目标目录
This commit is contained in:
@@ -72,6 +72,7 @@ export interface ExportOptions {
|
|||||||
exportEmojis?: boolean
|
exportEmojis?: boolean
|
||||||
exportVoiceAsText?: boolean
|
exportVoiceAsText?: boolean
|
||||||
excelCompactColumns?: boolean
|
excelCompactColumns?: boolean
|
||||||
|
sessionLayout?: 'shared' | 'per-session'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MediaExportItem {
|
interface MediaExportItem {
|
||||||
@@ -408,14 +409,15 @@ class ExportService {
|
|||||||
private async exportMediaForMessage(
|
private async exportMediaForMessage(
|
||||||
msg: any,
|
msg: any,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
mediaDir: string,
|
mediaRootDir: string,
|
||||||
|
mediaRelativePrefix: string,
|
||||||
options: { exportImages?: boolean; exportVoices?: boolean; exportEmojis?: boolean; exportVoiceAsText?: boolean }
|
options: { exportImages?: boolean; exportVoices?: boolean; exportEmojis?: boolean; exportVoiceAsText?: boolean }
|
||||||
): Promise<MediaExportItem | null> {
|
): Promise<MediaExportItem | null> {
|
||||||
const localType = msg.localType
|
const localType = msg.localType
|
||||||
|
|
||||||
// 图片消息
|
// 图片消息
|
||||||
if (localType === 3 && options.exportImages) {
|
if (localType === 3 && options.exportImages) {
|
||||||
const result = await this.exportImage(msg, sessionId, mediaDir)
|
const result = await this.exportImage(msg, sessionId, mediaRootDir, mediaRelativePrefix)
|
||||||
if (result) {
|
if (result) {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -429,13 +431,13 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
// 否则导出语音文件
|
// 否则导出语音文件
|
||||||
if (options.exportVoices) {
|
if (options.exportVoices) {
|
||||||
return this.exportVoice(msg, sessionId, mediaDir)
|
return this.exportVoice(msg, sessionId, mediaRootDir, mediaRelativePrefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动画表情
|
// 动画表情
|
||||||
if (localType === 47 && options.exportEmojis) {
|
if (localType === 47 && options.exportEmojis) {
|
||||||
const result = await this.exportEmoji(msg, sessionId, mediaDir)
|
const result = await this.exportEmoji(msg, sessionId, mediaRootDir, mediaRelativePrefix)
|
||||||
if (result) {
|
if (result) {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -447,9 +449,14 @@ class ExportService {
|
|||||||
/**
|
/**
|
||||||
* 导出图片文件
|
* 导出图片文件
|
||||||
*/
|
*/
|
||||||
private async exportImage(msg: any, sessionId: string, mediaDir: string): Promise<MediaExportItem | null> {
|
private async exportImage(
|
||||||
|
msg: any,
|
||||||
|
sessionId: string,
|
||||||
|
mediaRootDir: string,
|
||||||
|
mediaRelativePrefix: string
|
||||||
|
): Promise<MediaExportItem | null> {
|
||||||
try {
|
try {
|
||||||
const imagesDir = path.join(mediaDir, 'media', 'images')
|
const imagesDir = path.join(mediaRootDir, mediaRelativePrefix, 'images')
|
||||||
if (!fs.existsSync(imagesDir)) {
|
if (!fs.existsSync(imagesDir)) {
|
||||||
fs.mkdirSync(imagesDir, { recursive: true })
|
fs.mkdirSync(imagesDir, { recursive: true })
|
||||||
}
|
}
|
||||||
@@ -494,7 +501,7 @@ class ExportService {
|
|||||||
fs.writeFileSync(destPath, Buffer.from(base64Data, 'base64'))
|
fs.writeFileSync(destPath, Buffer.from(base64Data, 'base64'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
relativePath: `media/images/${fileName}`,
|
relativePath: path.posix.join(mediaRelativePrefix, 'images', fileName),
|
||||||
kind: 'image'
|
kind: 'image'
|
||||||
}
|
}
|
||||||
} else if (sourcePath.startsWith('file://')) {
|
} else if (sourcePath.startsWith('file://')) {
|
||||||
@@ -512,7 +519,7 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
relativePath: `media/images/${fileName}`,
|
relativePath: path.posix.join(mediaRelativePrefix, 'images', fileName),
|
||||||
kind: 'image'
|
kind: 'image'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,9 +533,14 @@ class ExportService {
|
|||||||
/**
|
/**
|
||||||
* 导出语音文件
|
* 导出语音文件
|
||||||
*/
|
*/
|
||||||
private async exportVoice(msg: any, sessionId: string, mediaDir: string): Promise<MediaExportItem | null> {
|
private async exportVoice(
|
||||||
|
msg: any,
|
||||||
|
sessionId: string,
|
||||||
|
mediaRootDir: string,
|
||||||
|
mediaRelativePrefix: string
|
||||||
|
): Promise<MediaExportItem | null> {
|
||||||
try {
|
try {
|
||||||
const voicesDir = path.join(mediaDir, 'media', 'voices')
|
const voicesDir = path.join(mediaRootDir, mediaRelativePrefix, 'voices')
|
||||||
if (!fs.existsSync(voicesDir)) {
|
if (!fs.existsSync(voicesDir)) {
|
||||||
fs.mkdirSync(voicesDir, { recursive: true })
|
fs.mkdirSync(voicesDir, { recursive: true })
|
||||||
}
|
}
|
||||||
@@ -540,7 +552,7 @@ class ExportService {
|
|||||||
// 如果已存在则跳过
|
// 如果已存在则跳过
|
||||||
if (fs.existsSync(destPath)) {
|
if (fs.existsSync(destPath)) {
|
||||||
return {
|
return {
|
||||||
relativePath: `media/voices/${fileName}`,
|
relativePath: path.posix.join(mediaRelativePrefix, 'voices', fileName),
|
||||||
kind: 'voice'
|
kind: 'voice'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,7 +568,7 @@ class ExportService {
|
|||||||
fs.writeFileSync(destPath, wavBuffer)
|
fs.writeFileSync(destPath, wavBuffer)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
relativePath: `media/voices/${fileName}`,
|
relativePath: path.posix.join(mediaRelativePrefix, 'voices', fileName),
|
||||||
kind: 'voice'
|
kind: 'voice'
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -582,9 +594,14 @@ class ExportService {
|
|||||||
/**
|
/**
|
||||||
* 导出表情文件
|
* 导出表情文件
|
||||||
*/
|
*/
|
||||||
private async exportEmoji(msg: any, sessionId: string, mediaDir: string): Promise<MediaExportItem | null> {
|
private async exportEmoji(
|
||||||
|
msg: any,
|
||||||
|
sessionId: string,
|
||||||
|
mediaRootDir: string,
|
||||||
|
mediaRelativePrefix: string
|
||||||
|
): Promise<MediaExportItem | null> {
|
||||||
try {
|
try {
|
||||||
const emojisDir = path.join(mediaDir, 'media', 'emojis')
|
const emojisDir = path.join(mediaRootDir, mediaRelativePrefix, 'emojis')
|
||||||
if (!fs.existsSync(emojisDir)) {
|
if (!fs.existsSync(emojisDir)) {
|
||||||
fs.mkdirSync(emojisDir, { recursive: true })
|
fs.mkdirSync(emojisDir, { recursive: true })
|
||||||
}
|
}
|
||||||
@@ -613,7 +630,7 @@ class ExportService {
|
|||||||
// 如果已存在则跳过
|
// 如果已存在则跳过
|
||||||
if (fs.existsSync(destPath)) {
|
if (fs.existsSync(destPath)) {
|
||||||
return {
|
return {
|
||||||
relativePath: `media/emojis/${fileName}`,
|
relativePath: path.posix.join(mediaRelativePrefix, 'emojis', fileName),
|
||||||
kind: 'emoji'
|
kind: 'emoji'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -623,7 +640,7 @@ class ExportService {
|
|||||||
const downloaded = await this.downloadFile(emojiUrl, destPath)
|
const downloaded = await this.downloadFile(emojiUrl, destPath)
|
||||||
if (downloaded) {
|
if (downloaded) {
|
||||||
return {
|
return {
|
||||||
relativePath: `media/emojis/${fileName}`,
|
relativePath: path.posix.join(mediaRelativePrefix, 'emojis', fileName),
|
||||||
kind: 'emoji'
|
kind: 'emoji'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1483,7 +1500,13 @@ class ExportService {
|
|||||||
|
|
||||||
// 媒体导出设置
|
// 媒体导出设置
|
||||||
const exportMediaEnabled = options.exportImages || options.exportVoices || options.exportEmojis
|
const exportMediaEnabled = options.exportImages || options.exportVoices || options.exportEmojis
|
||||||
const sessionDir = path.dirname(outputPath) // 会话目录,用于媒体导出
|
const outputDir = path.dirname(outputPath)
|
||||||
|
const outputBaseName = path.basename(outputPath, path.extname(outputPath))
|
||||||
|
const useSharedMediaLayout = options.sessionLayout === 'shared'
|
||||||
|
const mediaRelativePrefix = useSharedMediaLayout
|
||||||
|
? path.posix.join('media', outputBaseName)
|
||||||
|
: 'media'
|
||||||
|
const mediaRootDir = outputDir
|
||||||
|
|
||||||
// 媒体导出缓存
|
// 媒体导出缓存
|
||||||
const mediaCache = new Map<string, MediaExportItem | null>()
|
const mediaCache = new Map<string, MediaExportItem | null>()
|
||||||
@@ -1498,7 +1521,7 @@ class ExportService {
|
|||||||
if (mediaCache.has(mediaKey)) {
|
if (mediaCache.has(mediaKey)) {
|
||||||
mediaItem = mediaCache.get(mediaKey) || null
|
mediaItem = mediaCache.get(mediaKey) || null
|
||||||
} else {
|
} else {
|
||||||
mediaItem = await this.exportMediaForMessage(msg, sessionId, sessionDir, {
|
mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, {
|
||||||
exportImages: options.exportImages,
|
exportImages: options.exportImages,
|
||||||
exportVoices: options.exportVoices,
|
exportVoices: options.exportVoices,
|
||||||
exportEmojis: options.exportEmojis,
|
exportEmojis: options.exportEmojis,
|
||||||
@@ -1656,6 +1679,12 @@ class ExportService {
|
|||||||
fs.mkdirSync(outputDir, { recursive: true })
|
fs.mkdirSync(outputDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportMediaEnabled = options.exportMedia === true &&
|
||||||
|
Boolean(options.exportImages || options.exportVoices || options.exportEmojis)
|
||||||
|
const sessionLayout = exportMediaEnabled
|
||||||
|
? (options.sessionLayout ?? 'per-session')
|
||||||
|
: 'shared'
|
||||||
|
|
||||||
for (let i = 0; i < sessionIds.length; i++) {
|
for (let i = 0; i < sessionIds.length; i++) {
|
||||||
const sessionId = sessionIds[i]
|
const sessionId = sessionIds[i]
|
||||||
const sessionInfo = await this.getContactInfo(sessionId)
|
const sessionInfo = await this.getContactInfo(sessionId)
|
||||||
@@ -1668,10 +1697,10 @@ class ExportService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const safeName = sessionInfo.displayName.replace(/[<>:"/\\|?*]/g, '_')
|
const safeName = sessionInfo.displayName.replace(/[<>:"/\\|?*]/g, '_')
|
||||||
|
const useSessionFolder = sessionLayout === 'per-session'
|
||||||
|
const sessionDir = useSessionFolder ? path.join(outputDir, safeName) : outputDir
|
||||||
|
|
||||||
// 为每个会话创建单独的文件夹
|
if (useSessionFolder && !fs.existsSync(sessionDir)) {
|
||||||
const sessionDir = path.join(outputDir, safeName)
|
|
||||||
if (!fs.existsSync(sessionDir)) {
|
|
||||||
fs.mkdirSync(sessionDir, { recursive: true })
|
fs.mkdirSync(sessionDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -602,6 +602,87 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-layout-modal {
|
||||||
|
background: var(--card-bg);
|
||||||
|
padding: 28px 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
|
||||||
|
text-align: center;
|
||||||
|
width: min(520px, 90vw);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-options {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-option-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(var(--primary-rgb), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(var(--primary-rgb), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-actions {
|
||||||
|
margin-top: 18px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-cancel-btn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.export-result-modal {
|
.export-result-modal {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
padding: 32px 40px;
|
padding: 32px 40px;
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ interface ExportResult {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionLayout = 'shared' | 'per-session'
|
||||||
|
|
||||||
function ExportPage() {
|
function ExportPage() {
|
||||||
const [sessions, setSessions] = useState<ChatSession[]>([])
|
const [sessions, setSessions] = useState<ChatSession[]>([])
|
||||||
const [filteredSessions, setFilteredSessions] = useState<ChatSession[]>([])
|
const [filteredSessions, setFilteredSessions] = useState<ChatSession[]>([])
|
||||||
@@ -44,6 +46,7 @@ function ExportPage() {
|
|||||||
const [showDatePicker, setShowDatePicker] = useState(false)
|
const [showDatePicker, setShowDatePicker] = useState(false)
|
||||||
const [calendarDate, setCalendarDate] = useState(new Date())
|
const [calendarDate, setCalendarDate] = useState(new Date())
|
||||||
const [selectingStart, setSelectingStart] = useState(true)
|
const [selectingStart, setSelectingStart] = useState(true)
|
||||||
|
const [showMediaLayoutPrompt, setShowMediaLayoutPrompt] = useState(false)
|
||||||
|
|
||||||
const [options, setOptions] = useState<ExportOptions>({
|
const [options, setOptions] = useState<ExportOptions>({
|
||||||
format: 'excel',
|
format: 'excel',
|
||||||
@@ -212,7 +215,7 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startExport = async () => {
|
const runExport = async (sessionLayout: SessionLayout) => {
|
||||||
if (selectedSessions.size === 0 || !exportFolder) return
|
if (selectedSessions.size === 0 || !exportFolder) return
|
||||||
|
|
||||||
setIsExporting(true)
|
setIsExporting(true)
|
||||||
@@ -228,11 +231,12 @@ function ExportPage() {
|
|||||||
exportImages: options.exportMedia && options.exportImages,
|
exportImages: options.exportMedia && options.exportImages,
|
||||||
exportVoices: options.exportMedia && options.exportVoices,
|
exportVoices: options.exportMedia && options.exportVoices,
|
||||||
exportEmojis: options.exportMedia && options.exportEmojis,
|
exportEmojis: options.exportMedia && options.exportEmojis,
|
||||||
exportVoiceAsText: options.exportVoiceAsText, // 独立于 exportMedia
|
exportVoiceAsText: options.exportVoiceAsText, // ?????????exportMedia
|
||||||
excelCompactColumns: options.excelCompactColumns,
|
excelCompactColumns: options.excelCompactColumns,
|
||||||
|
sessionLayout,
|
||||||
dateRange: options.useAllTime ? null : options.dateRange ? {
|
dateRange: options.useAllTime ? null : options.dateRange ? {
|
||||||
start: Math.floor(options.dateRange.start.getTime() / 1000),
|
start: Math.floor(options.dateRange.start.getTime() / 1000),
|
||||||
// 将结束日期设置为当天的 23:59:59,以包含当天的所有消息
|
// ?????????????????????????????????23:59:59,??????????????????????????????
|
||||||
end: Math.floor(new Date(options.dateRange.end.getFullYear(), options.dateRange.end.getMonth(), options.dateRange.end.getDate(), 23, 59, 59).getTime() / 1000)
|
end: Math.floor(new Date(options.dateRange.end.getFullYear(), options.dateRange.end.getMonth(), options.dateRange.end.getDate(), 23, 59, 59).getTime() / 1000)
|
||||||
} : null
|
} : null
|
||||||
}
|
}
|
||||||
@@ -245,16 +249,28 @@ function ExportPage() {
|
|||||||
)
|
)
|
||||||
setExportResult(result)
|
setExportResult(result)
|
||||||
} else {
|
} else {
|
||||||
setExportResult({ success: false, error: `${options.format.toUpperCase()} 格式导出功能开发中...` })
|
setExportResult({ success: false, error: `${options.format.toUpperCase()} ???????????????????????????...` })
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('导出失败:', e)
|
console.error('????????????:', e)
|
||||||
setExportResult({ success: false, error: String(e) })
|
setExportResult({ success: false, error: String(e) })
|
||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false)
|
setIsExporting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startExport = () => {
|
||||||
|
if (selectedSessions.size === 0 || !exportFolder) return
|
||||||
|
|
||||||
|
if (options.exportMedia && selectedSessions.size > 1) {
|
||||||
|
setShowMediaLayoutPrompt(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout: SessionLayout = options.exportMedia ? 'per-session' : 'shared'
|
||||||
|
runExport(layout)
|
||||||
|
}
|
||||||
|
|
||||||
const getDaysInMonth = (date: Date) => {
|
const getDaysInMonth = (date: Date) => {
|
||||||
const year = date.getFullYear()
|
const year = date.getFullYear()
|
||||||
const month = date.getMonth()
|
const month = date.getMonth()
|
||||||
@@ -613,6 +629,43 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 媒体导出布局选择弹窗 */}
|
||||||
|
{showMediaLayoutPrompt && (
|
||||||
|
<div className="export-overlay" onClick={() => setShowMediaLayoutPrompt(false)}>
|
||||||
|
<div className="export-layout-modal" onClick={e => e.stopPropagation()}>
|
||||||
|
<h3>导出文件夹布局</h3>
|
||||||
|
<p className="layout-subtitle">检测到同时导出多个会话并包含媒体文件,请选择存放方式:</p>
|
||||||
|
<div className="layout-options">
|
||||||
|
<button
|
||||||
|
className="layout-option-btn primary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowMediaLayoutPrompt(false)
|
||||||
|
runExport('shared')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="layout-title">所有会话在同一文件夹</span>
|
||||||
|
<span className="layout-desc">媒体会按会话名归档到 media 子目录</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="layout-option-btn"
|
||||||
|
onClick={() => {
|
||||||
|
setShowMediaLayoutPrompt(false)
|
||||||
|
runExport('per-session')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="layout-title">每个会话一个文件夹</span>
|
||||||
|
<span className="layout-desc">每个会话单独包含导出文件和媒体</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="layout-actions">
|
||||||
|
<button className="layout-cancel-btn" onClick={() => setShowMediaLayoutPrompt(false)}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 导出进度弹窗 */}
|
{/* 导出进度弹窗 */}
|
||||||
{isExporting && (
|
{isExporting && (
|
||||||
<div className="export-overlay">
|
<div className="export-overlay">
|
||||||
|
|||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -333,6 +333,7 @@ export interface ExportOptions {
|
|||||||
exportEmojis?: boolean
|
exportEmojis?: boolean
|
||||||
exportVoiceAsText?: boolean
|
exportVoiceAsText?: boolean
|
||||||
excelCompactColumns?: boolean
|
excelCompactColumns?: boolean
|
||||||
|
sessionLayout?: 'shared' | 'per-session'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportProgress {
|
export interface ExportProgress {
|
||||||
|
|||||||
Reference in New Issue
Block a user