mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
一个简单的安卓岛
This commit is contained in:
@@ -47,7 +47,7 @@ interface ConfigSchema {
|
|||||||
|
|
||||||
// 通知
|
// 通知
|
||||||
notificationEnabled: boolean
|
notificationEnabled: boolean
|
||||||
notificationPosition: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
|
notificationPosition: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'
|
||||||
notificationFilterMode: 'all' | 'whitelist' | 'blacklist'
|
notificationFilterMode: 'all' | 'whitelist' | 'blacklist'
|
||||||
notificationFilterList: string[]
|
notificationFilterList: string[]
|
||||||
wordCloudExcludeWords: string[]
|
wordCloudExcludeWords: string[]
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ async function showAndSend(win: BrowserWindow, data: any) {
|
|||||||
|
|
||||||
// 更新位置
|
// 更新位置
|
||||||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize
|
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize
|
||||||
const winWidth = 344
|
const winWidth = position === 'top-center' ? 280 : 344
|
||||||
const winHeight = 114
|
const winHeight = 114
|
||||||
const padding = 20
|
const padding = 20
|
||||||
|
|
||||||
@@ -140,6 +140,10 @@ async function showAndSend(win: BrowserWindow, data: any) {
|
|||||||
let y = 0
|
let y = 0
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
|
case 'top-center':
|
||||||
|
x = (screenWidth - winWidth) / 2
|
||||||
|
y = padding
|
||||||
|
break
|
||||||
case 'top-right':
|
case 'top-right':
|
||||||
x = screenWidth - winWidth - padding
|
x = screenWidth - winWidth - padding
|
||||||
y = padding
|
y = padding
|
||||||
@@ -166,7 +170,7 @@ async function showAndSend(win: BrowserWindow, data: any) {
|
|||||||
win.showInactive() // 显示但不聚焦
|
win.showInactive() // 显示但不聚焦
|
||||||
win.setAlwaysOnTop(true, 'screen-saver') // 最高层级
|
win.setAlwaysOnTop(true, 'screen-saver') // 最高层级
|
||||||
|
|
||||||
win.webContents.send('notification:show', data)
|
win.webContents.send('notification:show', { ...data, position })
|
||||||
|
|
||||||
// 自动关闭计时器通常由渲染进程管理
|
// 自动关闭计时器通常由渲染进程管理
|
||||||
// 渲染进程发送 'notification:close' 来隐藏窗口
|
// 渲染进程发送 'notification:close' 来隐藏窗口
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ export function GlobalSessionMonitor() {
|
|||||||
if (!isCurrentSession && (!oldSession || newSession.lastTimestamp > oldSession.lastTimestamp)) {
|
if (!isCurrentSession && (!oldSession || newSession.lastTimestamp > oldSession.lastTimestamp)) {
|
||||||
// 这是新消息事件
|
// 这是新消息事件
|
||||||
|
|
||||||
// 免打扰、折叠群、折叠入口不弹通知
|
// 折叠群、折叠入口不弹通知
|
||||||
if (newSession.isMuted || newSession.isFolded) continue
|
if (newSession.isFolded) continue
|
||||||
if (newSession.username.toLowerCase().includes('placeholder_foldgroup')) continue
|
if (newSession.username.toLowerCase().includes('placeholder_foldgroup')) continue
|
||||||
|
|
||||||
// 1. 群聊过滤自己发送的消息
|
// 1. 群聊过滤自己发送的消息
|
||||||
|
|||||||
@@ -134,6 +134,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.top-center {
|
||||||
|
top: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -20px) scale(0.95);
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
transform: translate(-50%, 0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 灵动岛样式
|
||||||
|
border-radius: 40px !important;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
&.static {
|
||||||
|
border-radius: 40px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.16) !important;
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.16) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface NotificationToastProps {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
onClick: (sessionId: string) => void
|
onClick: (sessionId: string) => void
|
||||||
duration?: number
|
duration?: number
|
||||||
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
|
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'
|
||||||
isStatic?: boolean
|
isStatic?: boolean
|
||||||
initialVisible?: boolean
|
initialVisible?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||||
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Link, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, BellOff, Users, FolderClosed, UserCheck, Crown, Aperture } from 'lucide-react'
|
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Link, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, Users, FolderClosed, UserCheck, Crown, Aperture } from 'lucide-react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { useChatStore } from '../stores/chatStore'
|
import { useChatStore } from '../stores/chatStore'
|
||||||
@@ -377,7 +377,7 @@ const SessionItem = React.memo(function SessionItem({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`session-item ${isActive ? 'active' : ''} ${session.isMuted ? 'muted' : ''}`}
|
className={`session-item ${isActive ? 'active' : ''}`}
|
||||||
onClick={() => onSelect(session)}
|
onClick={() => onSelect(session)}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -394,9 +394,8 @@ const SessionItem = React.memo(function SessionItem({
|
|||||||
<div className="session-bottom">
|
<div className="session-bottom">
|
||||||
<span className="session-summary">{session.summary || '暂无消息'}</span>
|
<span className="session-summary">{session.summary || '暂无消息'}</span>
|
||||||
<div className="session-badges">
|
<div className="session-badges">
|
||||||
{session.isMuted && <BellOff size={12} className="mute-icon" />}
|
|
||||||
{session.unreadCount > 0 && (
|
{session.unreadCount > 0 && (
|
||||||
<span className={`unread-badge ${session.isMuted ? 'muted' : ''}`}>
|
<span className="unread-badge">
|
||||||
{session.unreadCount > 99 ? '99+' : session.unreadCount}
|
{session.unreadCount > 99 ? '99+' : session.unreadCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -414,7 +413,6 @@ const SessionItem = React.memo(function SessionItem({
|
|||||||
prevProps.session.unreadCount === nextProps.session.unreadCount &&
|
prevProps.session.unreadCount === nextProps.session.unreadCount &&
|
||||||
prevProps.session.lastTimestamp === nextProps.session.lastTimestamp &&
|
prevProps.session.lastTimestamp === nextProps.session.lastTimestamp &&
|
||||||
prevProps.session.sortTimestamp === nextProps.session.sortTimestamp &&
|
prevProps.session.sortTimestamp === nextProps.session.sortTimestamp &&
|
||||||
prevProps.session.isMuted === nextProps.session.isMuted &&
|
|
||||||
prevProps.isActive === nextProps.isActive
|
prevProps.isActive === nextProps.isActive
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1898,16 +1896,14 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
if (!status) return session
|
if (!status) return session
|
||||||
|
|
||||||
const nextIsFolded = status.isFolded ?? session.isFolded
|
const nextIsFolded = status.isFolded ?? session.isFolded
|
||||||
const nextIsMuted = status.isMuted ?? session.isMuted
|
if (nextIsFolded === session.isFolded) {
|
||||||
if (nextIsFolded === session.isFolded && nextIsMuted === session.isMuted) {
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
isFolded: nextIsFolded,
|
isFolded: nextIsFolded
|
||||||
isMuted: nextIsMuted
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes noti-enter-center {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px) scale(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes noti-exit {
|
@keyframes noti-exit {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -24,6 +36,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes noti-exit-center {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px) scale(0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
// Ensure the body background is transparent to let the rounded corners show
|
// Ensure the body background is transparent to let the rounded corners show
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -41,6 +65,10 @@ body {
|
|||||||
// New notification slides in
|
// New notification slides in
|
||||||
animation: noti-enter 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: noti-enter 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
|
|
||||||
|
&.anim-center {
|
||||||
|
animation: noti-enter-center 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-prev {
|
#notification-prev {
|
||||||
@@ -51,4 +79,8 @@ body {
|
|||||||
|
|
||||||
// Ensure it stays behind
|
// Ensure it stays behind
|
||||||
z-index: 0 !important;
|
z-index: 0 !important;
|
||||||
|
|
||||||
|
&.anim-center {
|
||||||
|
animation: noti-exit-center 0.5s cubic-bezier(0.33, 1, 0.68, 1) forwards;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,8 +6,9 @@ import './NotificationWindow.scss'
|
|||||||
export default function NotificationWindow() {
|
export default function NotificationWindow() {
|
||||||
const [notification, setNotification] = useState<NotificationData | null>(null)
|
const [notification, setNotification] = useState<NotificationData | null>(null)
|
||||||
const [prevNotification, setPrevNotification] = useState<NotificationData | null>(null)
|
const [prevNotification, setPrevNotification] = useState<NotificationData | null>(null)
|
||||||
|
const [position, setPosition] = useState<string>('top-right')
|
||||||
|
|
||||||
// We need a ref to access the current notification inside the callback
|
// We need a ref to access the current notification inside the callback
|
||||||
// without satisfying the dependency array which would recreate the listener
|
// without satisfying the dependency array which would recreate the listener
|
||||||
// Actually, setNotification(prev => ...) pattern is better, but we need the VALUE of current to set as prev.
|
// Actually, setNotification(prev => ...) pattern is better, but we need the VALUE of current to set as prev.
|
||||||
// So we use setNotification callback: setNotification(current => { ... return newNode })
|
// So we use setNotification callback: setNotification(current => { ... return newNode })
|
||||||
@@ -34,6 +35,11 @@ export default function NotificationWindow() {
|
|||||||
avatarUrl: data.avatarUrl
|
avatarUrl: data.avatarUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取位置配置
|
||||||
|
if (data.position) {
|
||||||
|
setPosition(data.position)
|
||||||
|
}
|
||||||
|
|
||||||
// Set previous to current (ref)
|
// Set previous to current (ref)
|
||||||
if (notificationRef.current) {
|
if (notificationRef.current) {
|
||||||
setPrevNotification(notificationRef.current)
|
setPrevNotification(notificationRef.current)
|
||||||
@@ -117,6 +123,7 @@ export default function NotificationWindow() {
|
|||||||
<div
|
<div
|
||||||
id="notification-prev"
|
id="notification-prev"
|
||||||
key={prevNotification.id}
|
key={prevNotification.id}
|
||||||
|
className={position === 'top-center' ? 'anim-center' : ''}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 2, // Match padding
|
top: 2, // Match padding
|
||||||
@@ -131,7 +138,7 @@ export default function NotificationWindow() {
|
|||||||
data={prevNotification}
|
data={prevNotification}
|
||||||
onClose={() => { }} // No-op for background item
|
onClose={() => { }} // No-op for background item
|
||||||
onClick={() => { }}
|
onClick={() => { }}
|
||||||
position="top-right"
|
position={position as any}
|
||||||
isStatic={true}
|
isStatic={true}
|
||||||
initialVisible={true}
|
initialVisible={true}
|
||||||
/>
|
/>
|
||||||
@@ -143,6 +150,7 @@ export default function NotificationWindow() {
|
|||||||
<div
|
<div
|
||||||
id="notification-current"
|
id="notification-current"
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
|
className={position === 'top-center' ? 'anim-center' : ''}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative', // Takes up space
|
position: 'relative', // Takes up space
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
@@ -154,7 +162,7 @@ export default function NotificationWindow() {
|
|||||||
data={notification}
|
data={notification}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
position="top-right"
|
position={position as any}
|
||||||
isStatic={true}
|
isStatic={true}
|
||||||
initialVisible={true}
|
initialVisible={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.settings-modal-overlay {
|
.settings-modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 41px;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const [transcribeLanguages, setTranscribeLanguages] = useState<string[]>(['zh'])
|
const [transcribeLanguages, setTranscribeLanguages] = useState<string[]>(['zh'])
|
||||||
|
|
||||||
const [notificationEnabled, setNotificationEnabled] = useState(true)
|
const [notificationEnabled, setNotificationEnabled] = useState(true)
|
||||||
const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'>('top-right')
|
const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'>('top-right')
|
||||||
const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all')
|
const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all')
|
||||||
const [notificationFilterList, setNotificationFilterList] = useState<string[]>([])
|
const [notificationFilterList, setNotificationFilterList] = useState<string[]>([])
|
||||||
const [filterSearchKeyword, setFilterSearchKeyword] = useState('')
|
const [filterSearchKeyword, setFilterSearchKeyword] = useState('')
|
||||||
@@ -1102,12 +1102,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<span className="custom-select-value">
|
<span className="custom-select-value">
|
||||||
{notificationPosition === 'top-right' ? '右上角' :
|
{notificationPosition === 'top-right' ? '右上角' :
|
||||||
notificationPosition === 'bottom-right' ? '右下角' :
|
notificationPosition === 'bottom-right' ? '右下角' :
|
||||||
notificationPosition === 'top-left' ? '左上角' : '左下角'}
|
notificationPosition === 'top-left' ? '左上角' :
|
||||||
|
notificationPosition === 'top-center' ? '中间上方' : '左下角'}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown size={14} className={`custom-select-arrow ${positionDropdownOpen ? 'rotate' : ''}`} />
|
<ChevronDown size={14} className={`custom-select-arrow ${positionDropdownOpen ? 'rotate' : ''}`} />
|
||||||
</div>
|
</div>
|
||||||
<div className={`custom-select-dropdown ${positionDropdownOpen ? 'open' : ''}`}>
|
<div className={`custom-select-dropdown ${positionDropdownOpen ? 'open' : ''}`}>
|
||||||
{[
|
{[
|
||||||
|
{ value: 'top-center', label: '中间上方' },
|
||||||
{ value: 'top-right', label: '右上角' },
|
{ value: 'top-right', label: '右上角' },
|
||||||
{ value: 'bottom-right', label: '右下角' },
|
{ value: 'bottom-right', label: '右下角' },
|
||||||
{ value: 'top-left', label: '左上角' },
|
{ value: 'top-left', label: '左上角' },
|
||||||
@@ -1117,7 +1119,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
key={option.value}
|
key={option.value}
|
||||||
className={`custom-select-option ${notificationPosition === option.value ? 'selected' : ''}`}
|
className={`custom-select-option ${notificationPosition === option.value ? 'selected' : ''}`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const val = option.value as 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
|
const val = option.value as 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'
|
||||||
setNotificationPosition(val)
|
setNotificationPosition(val)
|
||||||
setPositionDropdownOpen(false)
|
setPositionDropdownOpen(false)
|
||||||
await configService.setNotificationPosition(val)
|
await configService.setNotificationPosition(val)
|
||||||
|
|||||||
Reference in New Issue
Block a user