This commit is contained in:
godo 2024-07-23 19:02:10 +08:00
parent 2cb0c4de2b
commit 781af9b1f5
67 changed files with 3955 additions and 999 deletions

42
app.go
View file

@ -1,42 +0,0 @@
package main
import (
"context"
"godoos/cmd"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
go cmd.OsStart()
}
func (a *App) shutdown(ctx context.Context) {
cmd.OsStop()
}
func (a *App) OpenDirDialog() string {
path, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select Folder",
})
if err != nil {
runtime.LogErrorf(a.ctx, "Error: %+v\n", err)
}
return path
}
// Greet returns a greeting for the given name
// func (a *App) Greet(name string) string {
// return fmt.Sprintf("Hello %s, It's show time!", name)
// }

78
app/app.go Normal file
View file

@ -0,0 +1,78 @@
package app
import (
"context"
"errors"
"godoos/cmd"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
exDir string
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
go cmd.OsStart()
}
func (a *App) Shutdown(ctx context.Context) {
a.ctx = ctx
cmd.OsStop()
}
func (a *App) OpenDirDialog() string {
path, err := wruntime.OpenDirectoryDialog(a.ctx, wruntime.OpenDialogOptions{
Title: "Select Folder",
})
if err != nil {
wruntime.LogErrorf(a.ctx, "Error: %+v\n", err)
}
return path
}
func (a *App) GetAbsPath(path string) (string, error) {
var absPath string
var err error
if filepath.IsAbs(path) {
absPath = filepath.Clean(path)
} else {
absPath, err = filepath.Abs(filepath.Join(a.exDir, path))
if err != nil {
return "", err
}
}
absPath = strings.ReplaceAll(absPath, "/", string(os.PathSeparator))
//println("GetAbsPath:", absPath)
return absPath, nil
}
func (a *App) RestartApp() error {
if runtime.GOOS == "windows" {
name, err := os.Executable()
if err != nil {
return err
}
exec.Command(name, os.Args[1:]...).Start()
wruntime.Quit(a.ctx)
return nil
}
return errors.New("unsupported OS")
}
func (a *App) GetPlatform() string {
return runtime.GOOS
}

134
app/download.go Normal file
View file

@ -0,0 +1,134 @@
package app
import (
"context"
"path/filepath"
"time"
"github.com/cavaliergopher/grab/v3"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
func (a *App) DownloadFile(path string, url string) error {
absPath, err := a.GetAbsPath(path)
if err != nil {
return err
}
_, err = grab.Get(absPath, url)
if err != nil {
return err
}
return nil
}
type DownloadStatus struct {
resp *grab.Response
cancel context.CancelFunc
Name string `json:"name"`
Path string `json:"path"`
Url string `json:"url"`
Transferred int64 `json:"transferred"`
Size int64 `json:"size"`
Speed float64 `json:"speed"`
Progress float64 `json:"progress"`
Downloading bool `json:"downloading"`
Done bool `json:"done"`
}
var downloadList []*DownloadStatus
func existsInDownloadList(path string, url string) bool {
for _, ds := range downloadList {
if ds.Path == path || ds.Url == url {
return true
}
}
return false
}
func (a *App) PauseDownload(url string) {
for _, ds := range downloadList {
if ds.Url == url {
if ds.cancel != nil {
ds.cancel()
}
ds.resp = nil
ds.Downloading = false
ds.Speed = 0
break
}
}
}
func (a *App) ContinueDownload(url string) {
for _, ds := range downloadList {
if ds.Url == url {
if !ds.Downloading && ds.resp == nil && !ds.Done {
ds.Downloading = true
req, err := grab.NewRequest(ds.Path, ds.Url)
if err != nil {
ds.Downloading = false
break
}
// if PauseDownload() is called before the request finished, ds.Downloading will be false
// if the user keeps clicking pause and resume, it may result in multiple requests being successfully downloaded at the same time
// so we have to create a context and cancel it when PauseDownload() is called
ctx, cancel := context.WithCancel(context.Background())
ds.cancel = cancel
req = req.WithContext(ctx)
resp := grab.DefaultClient.Do(req)
if resp != nil && resp.HTTPResponse != nil &&
resp.HTTPResponse.StatusCode >= 200 && resp.HTTPResponse.StatusCode < 300 {
ds.resp = resp
} else {
ds.Downloading = false
}
}
break
}
}
}
func (a *App) AddToDownloadList(path string, url string) {
absPath, err := a.GetAbsPath(path)
if err != nil {
return
}
if !existsInDownloadList(absPath, url) {
downloadList = append(downloadList, &DownloadStatus{
resp: nil,
Name: filepath.Base(path),
Path: absPath,
Url: url,
Downloading: false,
})
a.ContinueDownload(url)
} else {
a.ContinueDownload(url)
}
}
func (a *App) DownloadLoop() {
ticker := time.NewTicker(500 * time.Millisecond)
go func() {
for {
<-ticker.C
for _, ds := range downloadList {
if ds.resp != nil {
ds.Transferred = ds.resp.BytesComplete()
ds.Size = ds.resp.Size()
ds.Speed = ds.resp.BytesPerSecond()
ds.Progress = 100 * ds.resp.Progress()
ds.Downloading = !ds.resp.IsComplete()
ds.Done = ds.resp.Progress() == 1
if !ds.Downloading {
ds.resp = nil
}
}
}
runtime.EventsEmit(a.ctx, "downloadList", downloadList)
}
}()
}

100
app/update.go Normal file
View file

@ -0,0 +1,100 @@
package app
import (
"archive/zip"
"bytes"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/minio/selfupdate"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
type ProgressReader struct {
reader io.Reader
total int64
err error
}
func (pr *ProgressReader) Read(p []byte) (n int, err error) {
n, err = pr.reader.Read(p)
pr.err = err
pr.total += int64(n)
return
}
func (a *App) UpdateApp(url string) (broken bool, err error) {
resp, err := http.Get(url)
if err != nil {
return false, err
}
defer resp.Body.Close()
pr := &ProgressReader{reader: resp.Body}
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
// update progress
go func() {
for {
<-ticker.C
wruntime.EventsEmit(a.ctx, "updateApp", &DownloadStatus{
Name: filepath.Base(url),
Path: "",
Url: url,
Transferred: pr.total,
Size: resp.ContentLength,
Speed: 0,
Progress: 100 * (float64(pr.total) / float64(resp.ContentLength)),
Downloading: pr.err == nil && pr.total < resp.ContentLength,
Done: pr.total == resp.ContentLength,
})
if pr.err != nil || pr.total == resp.ContentLength {
break
}
}
}()
var updateFile io.Reader = pr
// extract macos binary from zip
if strings.HasSuffix(url, ".zip") && runtime.GOOS == "darwin" {
zipBytes, err := io.ReadAll(pr)
if err != nil {
return false, err
}
archive, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
if err != nil {
return false, err
}
file, err := archive.Open("godoos.app/Contents/MacOS/godoos")
if err != nil {
return false, err
}
defer file.Close()
updateFile = file
}
// apply update
err = selfupdate.Apply(updateFile, selfupdate.Options{})
if err != nil {
if rerr := selfupdate.RollbackError(err); rerr != nil {
return true, rerr
}
return false, err
}
// restart app
if runtime.GOOS == "windows" {
name, err := os.Executable()
if err != nil {
return false, err
}
exec.Command(name, os.Args[1:]...).Start()
wruntime.Quit(a.ctx)
}
return false, nil
}

View file

@ -3,6 +3,7 @@ package cmd
import ( import (
"context" "context"
"godoos/libs" "godoos/libs"
"godoos/localchat"
"godoos/progress" "godoos/progress"
"log" "log"
"net/http" "net/http"
@ -11,7 +12,7 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
const serverAddress = ":56710" const serverAddress = ":56780"
var srv *http.Server var srv *http.Server
@ -50,6 +51,9 @@ func OsStart() {
router.HandleFunc("/file/writefile", HandleWriteFile).Methods(http.MethodPost) router.HandleFunc("/file/writefile", HandleWriteFile).Methods(http.MethodPost)
router.HandleFunc("/file/appendfile", HandleAppendFile).Methods(http.MethodPost) router.HandleFunc("/file/appendfile", HandleAppendFile).Methods(http.MethodPost)
router.HandleFunc("/file/watch", WatchHandler).Methods(http.MethodGet) router.HandleFunc("/file/watch", WatchHandler).Methods(http.MethodGet)
router.HandleFunc("/localchat/sse", localchat.SseHandler).Methods(http.MethodGet)
router.HandleFunc("/localchat/message", localchat.HandleMessage).Methods(http.MethodPost)
router.HandleFunc("/localchat/upload", localchat.MultiUploadHandler).Methods(http.MethodPost)
go progress.CheckActive(context.Background()) go progress.CheckActive(context.Background())
log.Printf("Listening on port: %v", serverAddress) log.Printf("Listening on port: %v", serverAddress)

View file

@ -14,10 +14,15 @@ declare module 'vue' {
BatteryPop: typeof import('./src/components/taskbar/BatteryPop.vue')['default'] BatteryPop: typeof import('./src/components/taskbar/BatteryPop.vue')['default']
Browser: typeof import('./src/components/builtin/Browser.vue')['default'] Browser: typeof import('./src/components/builtin/Browser.vue')['default']
Calendar: typeof import('./src/components/builtin/Calendar.vue')['default'] Calendar: typeof import('./src/components/builtin/Calendar.vue')['default']
Chat: typeof import('./src/components/localchat/Chat.vue')['default']
ChatContent: typeof import('./src/components/localchat/ChatContent.vue')['default']
ChatDomain: typeof import('./src/components/localchat/ChatDomain.vue')['default']
ChatEditor: typeof import('./src/components/localchat/ChatEditor.vue')['default']
ChatFoot: typeof import('./src/components/localchat/ChatFoot.vue')['default']
ChatNav: typeof import('./src/components/localchat/ChatNav.vue')['default']
CloseButton: typeof import('./src/components/ui/CloseButton.vue')['default'] CloseButton: typeof import('./src/components/ui/CloseButton.vue')['default']
CloseDesktop: typeof import('./src/components/desktop/CloseDesktop.vue')['default'] CloseDesktop: typeof import('./src/components/desktop/CloseDesktop.vue')['default']
ColorPicker: typeof import('./src/components/setting/ColorPicker.vue')['default'] ColorPicker: typeof import('./src/components/setting/ColorPicker.vue')['default']
Compony: typeof import('./src/components/install/compony.vue')['default']
Computer: typeof import('./src/components/computer/Computer.vue')['default'] Computer: typeof import('./src/components/computer/Computer.vue')['default']
ComputerNavBar: typeof import('./src/components/computer/ComputerNavBar.vue')['default'] ComputerNavBar: typeof import('./src/components/computer/ComputerNavBar.vue')['default']
ContextMenu: typeof import('./src/components/builtin/ContextMenu.vue')['default'] ContextMenu: typeof import('./src/components/builtin/ContextMenu.vue')['default']
@ -33,18 +38,23 @@ declare module 'vue' {
DialogTemp: typeof import('./src/components/window/DialogTemp.vue')['default'] DialogTemp: typeof import('./src/components/window/DialogTemp.vue')['default']
EditFileName: typeof import('./src/components/builtin/EditFileName.vue')['default'] EditFileName: typeof import('./src/components/builtin/EditFileName.vue')['default']
EditType: typeof import('./src/components/builtin/EditType.vue')['default'] EditType: typeof import('./src/components/builtin/EditType.vue')['default']
ElBadge: typeof import('element-plus/es')['ElBadge']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSpace: typeof import('element-plus/es')['ElSpace']
ElText: typeof import('element-plus/es')['ElText'] ElText: typeof import('element-plus/es')['ElText']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
Error: typeof import('./src/components/taskbar/Error.vue')['default'] Error: typeof import('./src/components/taskbar/Error.vue')['default']
@ -60,12 +70,10 @@ declare module 'vue' {
Install: typeof import('./src/components/install/Install.vue')['default'] Install: typeof import('./src/components/install/Install.vue')['default']
InstallCompony: typeof import('./src/components/install/InstallCompony.vue')['default'] InstallCompony: typeof import('./src/components/install/InstallCompony.vue')['default']
InstallMember: typeof import('./src/components/install/InstallMember.vue')['default'] InstallMember: typeof import('./src/components/install/InstallMember.vue')['default']
INstallMember: typeof import('./src/components/install/INstallMember.vue')['default']
InstallPerson: typeof import('./src/components/install/InstallPerson.vue')['default'] InstallPerson: typeof import('./src/components/install/InstallPerson.vue')['default']
LockDesktop: typeof import('./src/components/desktop/LockDesktop.vue')['default'] LockDesktop: typeof import('./src/components/desktop/LockDesktop.vue')['default']
Magnet: typeof import('./src/components/taskbar/Magnet.vue')['default'] Magnet: typeof import('./src/components/taskbar/Magnet.vue')['default']
MarkDown: typeof import('./src/components/builtin/MarkDown.vue')['default'] MarkDown: typeof import('./src/components/builtin/MarkDown.vue')['default']
Member: typeof import('./src/components/install/member.vue')['default']
MenuBar: typeof import('./src/components/window/MenuBar.vue')['default'] MenuBar: typeof import('./src/components/window/MenuBar.vue')['default']
MenuList: typeof import('./src/components/taskbar/MenuList.vue')['default'] MenuList: typeof import('./src/components/taskbar/MenuList.vue')['default']
MessageCenterPop: typeof import('./src/components/taskbar/MessageCenterPop.vue')['default'] MessageCenterPop: typeof import('./src/components/taskbar/MessageCenterPop.vue')['default']
@ -80,7 +88,6 @@ declare module 'vue' {
OpenWiteDialog: typeof import('./src/components/builtin/OpenWiteDialog.vue')['default'] OpenWiteDialog: typeof import('./src/components/builtin/OpenWiteDialog.vue')['default']
OsImage: typeof import('./src/components/builtin/OsImage.vue')['default'] OsImage: typeof import('./src/components/builtin/OsImage.vue')['default']
PdfViewer: typeof import('./src/components/builtin/PdfViewer.vue')['default'] PdfViewer: typeof import('./src/components/builtin/PdfViewer.vue')['default']
Person: typeof import('./src/components/install/person.vue')['default']
PictureStore: typeof import('./src/components/builtin/PictureStore.vue')['default'] PictureStore: typeof import('./src/components/builtin/PictureStore.vue')['default']
QuickLink: typeof import('./src/components/computer/QuickLink.vue')['default'] QuickLink: typeof import('./src/components/computer/QuickLink.vue')['default']
RectChosen: typeof import('./src/components/builtin/RectChosen.vue')['default'] RectChosen: typeof import('./src/components/builtin/RectChosen.vue')['default']

View file

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@liripeng/vue-audio-player": "^1.6.2", "@liripeng/vue-audio-player": "^1.6.2",
"dexie": "^4.0.8",
"element-plus": "^2.7.7", "element-plus": "^2.7.7",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",

View file

@ -1 +1 @@
46b0e0f4287bf8042cfe0a66114c4afc 1fa97678edcc780911c3d714233e97d2

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656439603095" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3879" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
</style></defs><path d="M510.944 960c-247.04 0-448-200.96-448-448s200.992-448 448-448c247.008 0 448 200.96 448 448S757.984 960 510.944 960zM510.944 128c-211.744 0-384 172.256-384 384 0 211.744 172.256 384 384 384 211.744 0 384-172.256 384-384C894.944 300.256 722.688 128 510.944 128z" p-id="3880" fill="#303133"></path><path d="M512 773.344c-89.184 0-171.904-40.32-226.912-110.624-10.88-13.92-8.448-34.016 5.472-44.896 13.888-10.912 34.016-8.48 44.928 5.472 42.784 54.688 107.136 86.048 176.512 86.048 70.112 0 134.88-31.904 177.664-87.552 10.784-14.016 30.848-16.672 44.864-5.888 14.016 10.784 16.672 30.88 5.888 44.864C685.408 732.32 602.144 773.344 512 773.344z" p-id="3881" fill="#303133"></path><path d="M368 515.2c-26.528 0-48-21.472-48-48l0-64c0-26.528 21.472-48 48-48s48 21.472 48 48l0 64C416 493.696 394.496 515.2 368 515.2z" p-id="3882" fill="#303133"></path><path d="M656 515.2c-26.496 0-48-21.472-48-48l0-64c0-26.528 21.504-48 48-48s48 21.472 48 48l0 64C704 493.696 682.496 515.2 656 515.2z" p-id="3883" fill="#303133"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

View file

@ -1,24 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { System } from "@/system/index.ts"; import { System } from "@/system/index.ts";
import { onMounted, computed } from "vue"; import { onMounted } from "vue";
// 使refcomputed
const isInstall: any = computed(() => {
const item = localStorage.getItem("isGodoOSInstall");
//
return item === "true";
});
onMounted(() => { onMounted(() => {
new System(); new System();
}); });
// router.beforeEach((to, _, next) => {
// console.log(to.name);
// if (!isInstall.value && to.name !== "install") {
// next("/install");
// } else {
// next();
// }
// });
</script> </script>
<template> <template>

View file

@ -0,0 +1,125 @@
[{
"title": "[666]",
"icon": "/image/chat/emoji/666.gif"
},
{
"title": "[比酷]",
"icon": "/image/chat/emoji/比酷.gif"
},
{
"title": "[比耶]",
"icon": "/image/chat/emoji/比耶.gif"
},
{
"title": "[超级崇拜]",
"icon": "/image/chat/emoji/超级崇拜.gif"
},
{
"title": "[嘲笑]",
"icon": "/image/chat/emoji/嘲笑.gif"
},
{
"title": "[吃屎]",
"icon": "/image/chat/emoji/吃屎.gif"
},
{
"title": "[崇拜]",
"icon": "/image/chat/emoji/崇拜.gif"
},
{
"title": "[大哭]",
"icon": "/image/chat/emoji/大哭.gif"
},
{
"title": "[大眼]",
"icon": "/image/chat/emoji/大眼.gif"
},
{
"title": "[调皮]",
"icon": "/image/chat/emoji/调皮.gif"
},
{
"title": "[发怒]",
"icon": "/image/chat/emoji/发怒.gif"
},
{
"title": "[好吃]",
"icon": "/image/chat/emoji/好吃.gif"
},
{
"title": "[惊吓]",
"icon": "/image/chat/emoji/惊吓.gif"
},
{
"title": "[可爱狗]",
"icon": "/image/chat/emoji/可爱狗.gif"
},
{
"title": "[哭笑]",
"icon": "/image/chat/emoji/哭笑.gif"
},
{
"title": "[酷笑]",
"icon": "/image/chat/emoji/酷笑.gif"
},
{
"title": "[流汗]",
"icon": "/image/chat/emoji/流汗.gif"
},
{
"title": "[露牙笑]",
"icon": "/image/chat/emoji/露牙笑.gif"
},
{
"title": "[拍打]",
"icon": "/image/chat/emoji/拍打.gif"
},
{
"title": "[思考]",
"icon": "/image/chat/emoji/思考.gif"
},
{
"title": "[天使]",
"icon": "/image/chat/emoji/天使.gif"
},
{
"title": "[吐槽]",
"icon": "/image/chat/emoji/吐槽.gif"
},
{
"title": "[吐了]",
"icon": "/image/chat/emoji/吐了.gif"
},
{
"title": "[委屈]",
"icon": "/image/chat/emoji/委屈.gif"
},
{
"title": "[委屈巴巴]",
"icon": "/image/chat/emoji/委屈巴巴.gif"
},
{
"title": "[想哭]",
"icon": "/image/chat/emoji/想哭.gif"
},
{
"title": "[想吐]",
"icon": "/image/chat/emoji/想吐.gif"
},
{
"title": "[小丑]",
"icon": "/image/chat/emoji/小丑.gif"
},
{
"title": "[斜笑]",
"icon": "/image/chat/emoji/斜笑.gif"
},
{
"title": "[斜眼笑]",
"icon": "/image/chat/emoji/斜眼笑.gif"
},
{
"title": "[疑问]",
"icon": "/image/chat/emoji/疑问.gif"
}
]

View file

@ -0,0 +1,75 @@
<template>
<el-row justify="space-between">
<el-col :span="2">
<chat-nav />
</el-col>
<el-col :span="6">
<chat-domain />
</el-col>
<el-col :span="16">
<chat-content />
</el-col>
</el-row>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
import { useLocalChatStore } from "@/stores/localchat";
import { ElMessage } from "element-plus";
import { getSystemConfig } from "@/system/config";
const store = useLocalChatStore();
const config = getSystemConfig();
let source:any;
onMounted(async () => {
await store.init()
init()
});
onUnmounted(() => {
if (source) {
source.close();
}
});
function init() {
if (typeof EventSource === "undefined") {
ElMessage.error("您的浏览器不支持SSE");
return;
}
const sseUrl = config.apiUrl + "/localchat/sse";
source = new EventSource(sseUrl);
//
source.onmessage = async function (event:any) {
//console.log("has message!");
const eventData = event.data; //
const jsonData = JSON.parse(eventData); //
//console.log(jsonData);
if (jsonData.type == "user_list") {
//store.userList = jsonData;
store.setUserList(jsonData.content);
//await nextTick();
}
if(jsonData.type == 'text'){
store.addText(jsonData);
}
if(jsonData.type == 'file'){
store.addFile(jsonData);
}
};
//
source.onopen = function () {
console.log("Connection opened.");
};
//
source.onerror = function () {
console.log("Connection closed.");
};
}
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,248 @@
<script setup lang="ts">
import { onMounted, inject, ref, nextTick, watch } from "vue";
import { useLocalChatStore } from "@/stores/localchat";
import { formatChatTime } from "@/util/common";
import Vditor from "vditor";
import "vditor/dist/index.css";
import { ElScrollbar } from "element-plus";
import { System } from "@/system";
const sys:any = inject<System>("system");
const store = useLocalChatStore();
const messageContainerRef = ref<InstanceType<typeof ElScrollbar>>();
const messageInnerRef = ref<HTMLDivElement>();
let isScrool = false;
onMounted(() => {
scrollToBottom();
});
const scrollToBottom = () => {
nextTick(() => {
if (messageContainerRef && messageContainerRef.value) {
// messageContainerRef.value!.setScrollTop(
// messageInnerRef.value!.clientHeight
// );
messageContainerRef.value.setScrollTop(messageInnerRef.value!.clientHeight);
}
});
};
watch(
() => store.msgList,
(_) => {
if(!isScrool){
scrollToBottom();
}
},
{
deep: true,
}
);
function replaceIconTags(text:any) {
// {**}
text = Vditor.md2html(text);
const regex = /\{\-(.*?)\-\}/g;
// 使replace
const replacedText = text.replace(regex, (_:any, p1:string) => {
// p1
return `<img src='/image/chat/emoji/${p1}.gif' style='width:30px;height:30px;' />`;
//return `![avatar](/image/chat/emoji/${p1}.gif)`
});
return replacedText;
}
async function scroll({ scrollTop }: { scrollTop: number }) {
if (store.msgList.length + 1 > store.pageSize && scrollTop < 1) {
isScrool = true;
await store.moreMsgList();
isScrool = false;
}
}
</script>
<template>
<div class="chatContentContainer" v-if="store.chatTargetId > 0">
<div class="message-area">
<el-scrollbar
max-height="100%"
class="scrollbar-container"
@scroll="scroll"
ref="messageContainerRef">
<div ref="messageInnerRef" class="message-wrap">
<div v-for="(item, index) in store.msgList" :key="index" :class="['message-block', item.isMe ? 'mine' : 'theirs']">
<div class="avatar-container">
<div class="icon-container">
<el-icon><component :is="item.isMe ? 'UserFilled' : 'Place'"/></el-icon>
</div>
</div>
<div class="content">
<div
v-if="item.type === 'text'"
v-html="replaceIconTags(item.content)"
class="message-content">
</div>
<div v-if="item.type === 'file'">
<div class="file-bubble">
<div class="file-content" v-for="el in item.content" @click="sys.openFile(el.path)">
<div class="file-icon"><FileIcon :file="el" /></div>
<div class="file-name">{{el.name}}</div>
</div>
</div>
</div>
<div class="timestamp text-grey">{{ formatChatTime(item.createdAt) }}</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
<ChatFoot class="mt-20px"></ChatFoot>
</div>
<div class="no-message-container" v-else>
<Vue3Lottie animationLink="/bot/localchat.json" :height="300" :width="300" />
</div>
</template>
<style scoped lang="scss">
$win10-blue: #0078d7;
$win10-light-blue: #c7e8ff;
$win10-grey: #afafaf;
$win10-light-grey: #f2f2f2;
.chatContentContainer {
flex: 1;
}
.message-area {
height: 420px;
}
.scrollbar-container {
width: 100%;
}
.message-wrap {
padding: 10px;
}
.message-block {
display: flex;
margin-bottom: 10px;
align-items: flex-end; //
flex-direction: row;
}
.avatar-container {
width: 40px;
height: 40px;
margin-right: 10px;
}
.icon-container {
width: 30px;
height: 30px;
border-radius: 50%;
overflow: hidden;
border: 1px solid $win10-grey;
display: flex;
justify-content: center;
align-items: center;
}
.content {
display: flex;
flex-direction: column;
justify-content: space-between;
max-width: calc(100% - 150px);
}
.message-content {
background: $win10-light-grey;
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
color: $win10-grey;
word-break: break-word;
}
.timestamp {
font-size: 12px;
color: $win10-grey;
}
.mine {
// 'mine'
justify-content: flex-end;
.content {
// 'mine'
// justify-content: flex-end; justify-content
align-items: flex-end; //
}
.message-content {
background-color: $win10-blue;
color: $win10-light-blue;
border-radius: 12px 2px 2px 2px; //
}
.avatar-container {
order: 1;
margin-left: 10px;
margin-top:-20px;
}
}
.theirs {
.content {
// 使
align-items: flex-end;
}
.message-content {
border-radius: 2px 12px 2px 2px;
}
}
.no-message-container {
height: 100%;
margin: 120px auto;
}
.file-bubble {
background-color: #f0f0f0; /* 背景色,可以根据需要调整 */
border-radius: 10px; /* 圆角,让框看起来更柔和 */
padding: 10px; /* 内边距,给内容一些空间 */
margin-bottom: 10px; /* 气泡间的外边距,使它们看起来不紧凑 */
max-width: 100%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 添加阴影效果,增强立体感 */
}
.file-content {
display: flex;
align-items: center;
height:36px;
line-height:36px;
gap: 3px;
}
.file-content:hover {
background-color: #e0e0e0; /* 改变背景色,悬停时更浅或更深,根据设计调整 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* 增强阴影效果,使气泡在悬停时更加突出 */
transition: all 0.3s ease; /* 添加过渡效果,使变化平滑 */
}
.file-icon {
width: 18px;
height: 18px;
}
.file-name {
flex: 1;
overflow-wrap: break-word;
word-break: break-all;
white-space: normal;
padding-left: 5px;
/* 可选:限制文件名行数,如果需要 */
/* max-height: 2em; */
/* display: -webkit-box; */
/* -webkit-line-clamp: 2; */
/* -webkit-box-orient: vertical; */
/* overflow: hidden; */
}
</style>

View file

@ -0,0 +1,199 @@
<template>
<div class="win11-msg-container">
<el-scrollbar>
<div v-if="store.navId < 2" class="user-list-area">
<el-row
class="user-list"
justify="space-around"
v-for="(msg, key) in store.contentList"
:key="key"
v-if="store.contentList.length > 0"
>
<!-- <el-col :span="5" class="avatar-col">
<el-icon :size="22" class="avatar">
<Place />
</el-icon>
</el-col> -->
<el-col :span="22" :class="msg.targetIp == store.chatTargetIp ? 'user-item active' : 'user-item'" @click="store.setChatId(msg.targetIp)">
<span class="username">
<el-icon :size="12">
<Place />
</el-icon>
{{ msg.reciperInfo.hostname }}
</span>
<span class="msg">{{ msg.content }}</span>
<span class="userip" v-if="msg.readNum > 0">
<el-badge :value="msg.readNum" :offset="[-3,8]">
{{ formatChatTime(msg.createdAt) }}</el-badge>
</span>
<span class="userip" v-else>
{{ formatChatTime(msg.createdAt) }}
</span>
</el-col>
</el-row>
<el-empty v-else :image-size="100" description="消息列表为空" />
</div>
<div v-else class="user-list-area">
<el-row
justify="start">
<el-icon :size="18" @click="store.refreshUserList">
<RefreshRight />
</el-icon>
</el-row>
<el-row
class="user-list"
justify="space-around"
v-for="(user, key) in store.userList"
:key="key"
v-if="store.userList.length > 0"
>
<el-col :span="5" class="avatar-col">
<el-icon :size="22" class="avatar">
<Monitor />
</el-icon>
<el-icon v-if="user.isOnline" :size="16" class="status-icon online">
<CircleCheck />
</el-icon>
<el-icon v-else :size="16" class="status-icon offline">
<Warning />
</el-icon>
</el-col>
<el-col :span="19" class="user-item" @click="store.setChatId(user.ip)">
<span class="username">{{ user.hostname }}</span>
<span class="userip">{{ user.ip }}</span>
</el-col>
</el-row>
<el-empty v-else :image-size="100" description="好友列表为空" />
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { useLocalChatStore } from "@/stores/localchat";
import { formatChatTime } from "@/util/common";
const store = useLocalChatStore();
</script>
<style scoped lang="scss">
.win11-msg-container {
height: 100vh; /* 假设顶部有100px的导航栏 */
width: 100%;
border-right: 1px solid rgba(238, 238, 238, 0.5); /* 更柔和的边框 */
border-top: 1px solid rgba(238, 238, 238, 0.5);
border-radius: 0 12px 0 0; /* Win11风格的圆角 */
background-color: #f8f8f8; /* 使用更亮的淡灰色背景 */
overflow: hidden; /* 清除滚动条溢出 */
}
.el-scrollbar__wrap {
padding: 16px; /* 内容区域的内边距 */
}
.message-item {
display: flex;
align-items: flex-start;
&.self {
justify-content: flex-end; /* 自身消息右对齐 */
}
}
.bubble {
padding: 8px 12px;
border-radius: 12px; /* 圆角 */
max-width: 75%;
background-color: #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&.bubble--right {
background-color: #e8eaed; /* 更接近Win11的发送方消息背景色 */
}
}
.user-list-area {
width: 94%;
margin: 3%;
}
.user-list {
background-color: #f8f8f8; //
border-radius: 8px; //
margin-bottom: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); //
.avatar-col {
display: flex;
justify-content: center;
align-items: center; //
}
.active{
background-color: #e8eaed;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.user-item {
display: flex;
align-items: flex-start;
flex-direction: column; /* 改变为列方向布局 */
gap: 4px; /* 行间间距 */
padding: 4px 0;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.avatar {
border-radius: 50%; /* 头像圆角 */
}
.username {
text-align: left; // username
font-size: 12px;
color: #333;
overflow: hidden;
width:95%;
text-overflow: ellipsis;
white-space: nowrap;
}
.msg {
text-align: left; // username
font-size: 11px;
color: #666;
overflow: hidden;
width:95%;
text-overflow: ellipsis;
white-space: nowrap;
}
.userip {
text-align: right; // userip
margin-left: auto; // 使userip
font-size: 11px;
padding-right: 5px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.status-icon {
margin-left: 2px; //
margin-top:12px;
}
.online {
color: green; // 线
}
.offline {
color: red; // 线
}
}
/* 空状态调整 */
.el-empty {
margin: auto;
}
</style>

View file

@ -0,0 +1,96 @@
<template>
<!-- 聊天编辑区域 -->
<div
class="edit-box"
@dragover.prevent
@drop.prevent="handleDrop"
>
<el-input
type="textarea"
:rows="4"
class="message-input"
@keydown.enter="keyDown($event)"
v-model="store.sendInfo" />
<el-tooltip placement="top" content="按enter键发送按ctrl+enter键换行">
<el-icon :size="22" class="win11-chat-send-button" @click="store.sendMsg()">
<Promotion />
</el-icon>
</el-tooltip>
</div>
</template>
<script setup lang="ts">
import { useLocalChatStore } from "@/stores/localchat"
import { notifyError } from "@/util/msg";
const store = useLocalChatStore()
//
function keyDown(event: any) {
if (event.ctrlKey && event.keyCode === 13) {
store.sendInfo = store.sendInfo + "\n"
} else if (event.keyCode === 13) {
event.preventDefault() //
send()
return false
}
}
const handleDrop = (event:any) => {
const frompathArrStr = event?.dataTransfer?.getData('frompath');
event.preventDefault()
const files = JSON.parse(frompathArrStr) as string[];
if (files && files.length > 0) {
//
console.log('Files dropped:', files);
store.uploadFile(files)
}
};
function send(){
if(!store.hostInfo || !store.hostInfo.ip){
notifyError("Please wait for a moment");
return;
}
store.sendMsg()
}
</script>
<style scoped>
.edit-box {
position: relative; /* 为容器添加相对定位,以便子元素可以相对于它进行定位 */
}
.message-input {
resize: none; /* 禁止调整输入框大小,保持布局稳定 */
}
.win11-chat-send-button {
position: absolute;
bottom: 5px;
right: 5px;
width: 30px; /* 缩小宽度 */
height: 30px; /* 减小高度 */
border-radius: 50%; /* 较小的圆角 */
background-color: #E8F0FE; /* 浅蓝色符合Win11的轻量风格 */
color: #0078D4; /* 使用Win11的强调色作为文字颜色 */
font-weight: bold;
border: 1px solid #B3D4FC; /* 添加边框,保持简洁风格 */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); /* 轻微阴影 */
transition: all 0.2s ease; /* 快速过渡效果 */
}
.win11-chat-send-button:hover {
background-color: #D1E4FF; /* 悬浮时颜色略深,保持浅色调 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 稍微增强阴影 */
}
.win11-chat-send-button:active {
background-color: #B3D4FC; /* 按下时颜色更深,但依然保持清新 */
box-shadow: 0 1px 2px rgba(0, 0, 0.1); /* 回复初始阴影 */
transform: translateY(1px); /* 微小下移,模拟按下 */
}
</style>

View file

@ -0,0 +1,165 @@
<template>
<footer class="footer-container">
<!--工具栏-->
<el-row type="flex" class="toolbar">
<el-popover
placement="top"
popper-class="chat-icon-popover"
trigger="click"
>
<template #reference>
<div class="emoji-button">
<img
width="24"
height="24"
class="emoji-image"
src="/image/chat/emoji.svg"
/>
</div>
</template>
<el-scrollbar class="emoji-scroll">
<ul class="emoji-list-container">
<li
v-for="item in store.emojiList"
:key="item.title"
class="p-5px list-none hover:animate-heart-beat animate-count-animated animate-duration-1s cursor-pointer"
:title="item.title"
>
<img
width="30"
height="30"
:src="item.icon"
@click="selectIcon(item.icon)"
/>
</li>
</ul>
</el-scrollbar>
</el-popover>
<div class="upload-button" @click="selectImg()">
<el-icon :size="22">
<Picture />
</el-icon>
</div>
<div class="upload-button" @click="selectFile()">
<el-icon :size="22">
<Link />
</el-icon>
</div>
<div class="upload-button" @click="store.clearMsg()">
<el-icon :size="22">
<Delete />
</el-icon>
</div>
</el-row>
<ChatEditor></ChatEditor>
</footer>
</template>
<script setup lang="ts">
import { useLocalChatStore } from "@/stores/localchat";
import { useChooseStore } from "@/stores/choose";
import { toRaw, watch } from "vue";
const store = useLocalChatStore();
const choose = useChooseStore();
//const editor = ref(null)
const imgExt = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"];
//
function selectIcon(icon: string) {
store.sendInfo +=
"{-" + icon.replace("/image/chat/emoji/", "").replace(".gif", "") + "-}";
}
function selectImg() {
choose.select("选择图片", imgExt);
}
function selectFile() {
choose.select("选择文件", "*");
}
watch(
() => choose.path,
(newVal, _) => {
//console.log("userList :", newVal);
const paths = toRaw(newVal)
if(paths.length > 0){
store.uploadFile(paths).then(() => {
choose.path = []
})
}
},
{ deep: true } // deep: true
);
</script>
<style lang="scss" scoped>
.footer-container {
padding-left: 20px;
padding-right: 20px;
}
.toolbar {
margin-bottom: 10px;
}
.opacity-0 {
opacity: 0;
display: none;
width: 0;
height: 0;
}
.emoji-button {
font-size: 20px;
cursor: pointer;
}
.emoji-image {
display: block;
width: 24px;
height: 24px;
}
.emoji-scroll {
height: 150px;
}
.emoji-list {
padding: 0;
list-style-type: none;
flex-wrap: wrap;
}
.emoji-list-item {
padding: 5px;
cursor: pointer;
animation-duration: 1s;
&.hover:animate-heart-beat {
animation-name: heart-beat;
}
}
.emoji-img {
width: 30px;
height: 30px;
}
.emoji-list-container {
margin: 0; /* 对应 m0 */
padding: 0; /* 对应 p0 */
display: flex; /* 对应 flex */
flex-wrap: wrap; /* 对应 flex-wrap */
}
.upload-button {
margin-left: 15px;
cursor: pointer;
}
.file-input {
opacity: 0;
}
.answer-editor {
/* Assuming this class already exists or you define it elsewhere */
}
</style>

View file

@ -0,0 +1,93 @@
<template>
<el-space direction="vertical" :size="20" class="win11-chat-nav">
<div class="nav-item" v-for="item in store.navList" :key="item.index">
<el-button
:icon="item.icon"
:class="store.navId === item.index ? 'active' : ''"
dark
circle
@click="store.handleSelect(item.index)"
/>
</div>
</el-space>
</template>
<script setup lang="ts">
import { useLocalChatStore } from "@/stores/localchat";
const store = useLocalChatStore();
</script>
<style lang="scss" scoped>
.win11-chat-nav {
height: 100vh;
background-color: #f8f8f8; /* 使用更亮的淡灰色更接近Win11的背景色 */
border-right: 1px solid rgba(230, 230, 230, 0.5); /* 更浅的边框颜色 */
padding: 16px;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
overflow-y: auto;
display: flex;
flex-direction: column;
/* 添加全局字体样式以匹配Win11 */
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
.win11-chat-nav .nav-item {
/* 假定每个按钮的基类 */
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 3px;
border-radius: 50%; /* 圆角 */
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 渐变阴影 */
transition: all 0.2s ease-in-out;
&:hover {
background-color: #f0f0f0; /* 鼠标悬停时的轻微颜色变化 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
&.active {
/* 加强背景色对比使用Win11的强调色或品牌色 */
background-color: #4579a1; /* 示例颜色,可根据设计调整 */
color: white; /* 文字颜色反转,确保可读性 */
/* 增加外边框以进一步区分 */
border: 2px solid #005a9c; /* 较深的强调色作为边框 */
border-radius: 50%;
/* 内发光效果,让按钮看起来更‘活跃’ */
box-shadow: 0 5px 8px rgba(0, 120, 212, 0.5) inset;
/* 微动效,当状态改变时给予用户反馈 */
transform: scale(1.02);
transition: transform 0.2s cubic-bezier(0.2, 0.4, 0.6, 1);
/* 确保文字在按下时不会因按钮尺寸变化而偏移 */
transition-property: background-color, box-shadow, transform, color;
/* 如果按钮包含图标,可以考虑为图标也添加强调效果,例如改变颜色 */
.el-icon {
color: inherit; /* 或指定特定强调色 */
}
/* 为了平滑的过渡确保所有相关属性都被包含在transition中 */
}
/* 考虑到按钮是圆形且使用了 `circle` 属性,确保图标和背景颜色调整得当 */
&.active .el-button {
background-color: transparent !important; /* 确保背景色不影响图标颜色 */
}
/* 图标颜色调整确保在active状态下足够突出 */
&.active .el-icon {
color: #ffffff; /* 确保图标颜色与背景对比鲜明 */
}
/* 非活动状态的悬停效果,保持与.active状态的区分 */
.nav-item:hover:not(.active) {
/* 调整以与.active状态区分例如使用较浅的颜色 */
background-color: #eaeaea;
}
}
</style>

110
frontend/src/stores/db.ts Normal file
View file

@ -0,0 +1,110 @@
import Dexie from 'dexie'
export type ChatTable = 'chatuser' | 'chatmsg' | 'chatfile'
export const dbInit:any = new Dexie('GodoOSDatabase');
dbInit.version(1).stores({
chatuser:'++id,ip,hostname,userName,avatar,mobile,nickName,isOnline,updatedAt,createdAt',
chatmsg:'++id,targetId,targetIp,senderInfo,reciperInfo,content,type,status,isRead,isMe,readAt,createdAt',
chatfile:'++id,msgId,fileName,fileSize,fileType,fileUrl,filePath,fileExt'
});
export const db = {
async getMaxId(tableName: ChatTable) {
const data = await dbInit[tableName].orderBy('id').reverse().first()
if (!data) {
return 0
} else {
return data.id
}
},
async getInsertId(tableName: ChatTable) {
const id: any = await this.getMaxId(tableName)
return id + 1
},
async getPage(tableName: ChatTable, page?: number, size?: number) {
page = (!page || page < 1) ? 1 : page
size = size ? size : 10
const offset = (page - 1) * size
return dbInit[tableName]
.orderBy("id")
.reverse()
.offset(offset)
.limit(size)
.toArray();
},
async getAll(tableName: ChatTable) {
return dbInit[tableName].toArray()
},
async count(tableName: ChatTable) {
return dbInit[tableName].count()
},
async countSearch(tableName: ChatTable, whereObj?: any) {
return dbInit[tableName].where(whereObj).count()
},
async pageSearch(tableName: ChatTable, page?: number, size?: number, whereObj?: any) {
page = (!page || page < 1) ? 1 : page
size = size ? size : 10
const offset = (page - 1) * size
//console.log(whereObj)
return dbInit[tableName]
.where(whereObj)
.reverse()
.offset(offset)
.limit(size)
.toArray();
},
async filter(tableName: ChatTable, filterFunc : any) {
return dbInit[tableName].filter(filterFunc).toArray()
},
table(tableName: ChatTable) {
return dbInit[tableName]
},
async getOne(tableName: ChatTable, Id: number) {
return dbInit[tableName].get(Id)
},
async getRow(tableName: ChatTable, fieldName: string, val: any){
return dbInit[tableName].where(fieldName).equals(val).first()
},
async get(tableName: ChatTable, whereObj : any) {
//console.log(whereObj)
const data = await dbInit[tableName].where(whereObj).first()
//console.log(data)
return data? data : false
},
async rows(tableName: ChatTable, whereObj: any) {
return dbInit[tableName].where(whereObj).toArray()
},
async field(tableName: ChatTable, whereObj: any, field: string) {
const data = await this.get(tableName, whereObj)
return data ? data[field] : false
},
async getValue(tableName: ChatTable, fieldName: string, val: any, fName : string) {
const row = await this.getRow(tableName, fieldName, val);
return row[fName]
},
async getByField(tableName: ChatTable, fieldName: string, val: any) {
return dbInit[tableName].where(fieldName).equals(val).toArray()
},
async addOne(tableName: ChatTable, data: any) {
return dbInit[tableName].add(data)
},
async addAll(tableName: ChatTable, data: any) {
return dbInit[tableName].bulkAdd(data)
},
async update(tableName: ChatTable, Id?: number, updates?: any) {
return dbInit[tableName].update(Id, updates)
},
async modify(tableName: ChatTable, fieldName: string, val: any, updates: any) {
return dbInit[tableName].where(fieldName).equals(val).modify(updates)
},
async delete(tableName: ChatTable, Id?: number) {
return dbInit[tableName].delete(Id)
},
async deleteByField(tableName: ChatTable, fieldName: string, val: any) {
return dbInit[tableName].where(fieldName).equals(val).delete()
},
async clear(tableName: ChatTable) {
return dbInit[tableName].clear()
},
}

View file

@ -0,0 +1,464 @@
import { defineStore } from 'pinia'
import emojiList from "@/assets/emoji.json"
import { ref, toRaw, inject } from "vue";
import { db } from './db'
import { System,dirname } from "@/system";
import { getSystemConfig } from "@/system/config";
import { isBase64, base64ToBuffer } from "@/util/file";
import { notifyError, notifySuccess } from "@/util/msg";
export const useLocalChatStore = defineStore('localChatStore', () => {
const config = getSystemConfig();
const sys = inject<System>("system");
const userList:any = ref([])
const msgList:any = ref([])
const contentList:any = ref([])
const hostInfo:any = ref({})
const showChooseFile = ref(false)
const currentPage = ref(1)
const pageSize = ref(50)
const navList = ref([
{ index: 1, lable: "消息列表", icon: "ChatDotRound", type:"success" },
{ index: 2, lable: "用户列表", icon: "UserFilled", type: "info" },
])
const navId = ref(1)
const sendInfo = ref("")
const chatTargetId = ref(0)
const chatTargetIp = ref("")
const handleSelect = (key: number) => {
navId.value = key;
};
const setChatId = async(ip : string) => {
//console.log(ip)
//chatTargetId.value = id
chatTargetIp.value = ip
const data = await db.get("chatuser", {ip : ip})
if(!data)return;
chatTargetId.value = data.id
clearContentList(data.id)
currentPage.value = 1
await getMsgList()
}
const initContentList = async () => {
const list:any = {}
const msgAll = await db.getAll('chatmsg')
msgAll.forEach((d : any) => {
if(!d.isMe){
if (!list[d.targetIp]){
list[d.targetIp] = []
}
list[d.targetIp].push(d)
}
})
const res = []
for(const p in list){
const chatArr = list[p]
let readNum = 0
chatArr.forEach((d:any) => {
if(!d.isRead){
readNum++
}
})
const last = chatArr.pop()
last.readNum = readNum
res.push(last)
}
contentList.value = res.sort((a, b) => b.createdAt - a.createdAt);
}
const clearContentList = (targetId:number) => {
contentList.value.forEach((d : any) => {
if(d.targetId === targetId) {
d.readNum = 0
}
})
}
const clearMsg = async () => {
if (chatTargetIp.value === '') return
await db.deleteByField('chatmsg','targetIp', chatTargetIp.value)
msgList.value = []
}
const updateContentList = async (msg:any) => {
if(msg.isMe)return;
const has = contentList.value.find((d:any) => d.targetIp === msg.targetIp)
if(has){
contentList.value.forEach((d: any, index: number) => {
if (d.targetIp === msg.targetIp) {
if (!msg.isRead) {
msg.readNum = d.readNum + 1;
} else {
msg.readNum = 0;
}
// 直接替换数组中的元素以触发更新
contentList.value.splice(index, 1, msg);
}
});
//console.log(contentList.value)
}else{
if(msg.isRead){
msg.readNum = 0
}else{
msg.readNum = 1
}
contentList.value.unshift(msg)
//console.log(contentList.value)
}
contentList.value = contentList.value.sort((a: any, b: any) => b.createdAt - a.createdAt)
}
const init = async() => {
await getUserList()
await initUserList()
await initContentList()
}
const initUserList = async() => {
if(userList.value.length > 0) {
const updates : any = []
userList.value.forEach((d: any) => {
if (d.isOnline) {
updates.push({
key: d.id,
changes: {
isOnline: false
}
})
}
});
await db.table('chatuser').bulkUpdate(updates)
}
}
const refreshUserList = async () => {
await db.clear('chatuser')
userList.value = []
}
const getMsgList = async () => {
if (chatTargetId.value < 1)return;
//msgList.value = await db.getByField('chatmsg', 'targetId', chatTargetId.value)
//msgList.value = await db.pageSearch('chatmsg', currentPage.value, pageSize.value, { targetId: chatTargetId.value })
const offset = (currentPage.value - 1) * pageSize.value
const list = await db.table('chatmsg')
.where({ targetIp: chatTargetIp.value })
.desc()
.offset(offset)
.limit(pageSize.value)
.toArray();
list.sort((a:any,b:any) => a.id > b.id)
msgList.value = list;
}
const moreMsgList = async() => {
if (chatTargetId.value < 1) return;
//const list = await db.pageSearch('chatmsg', currentPage.value + 1, pageSize.value, { targetId: chatTargetId.value })
const offset = currentPage.value * pageSize.value
const list = await db.table('chatmsg')
.where({ targetIp: chatTargetIp.value })
.desc()
.offset(offset)
.limit(pageSize.value)
.toArray();
if(list && list.length > 0) {
list.sort((a: any, b: any) => a.id > b.id)
currentPage.value = currentPage.value + 1
msgList.value = [...list, ...msgList.value];
}
}
const getUserList = async () => {
const list = await db.getAll('chatuser')
list.sort((a: any, b: any) => a.updatedAt > b.updatedAt)
userList.value = list
}
const setUserList = async (data:any) => {
//console.log(data)
if(data.length < 1){
return
}
hostInfo.value = data[0]
data.shift()
const ips:any = []
userList.value.forEach((d : any) => {
ips.push(d.ip)
});
const has:any = []
const nothas:any = []
data.forEach((d:any) => {
if(ips.includes(d.ip)){
has.push(d.ip)
}else{
nothas.push(d)
}
})
if(has.length > 0) {
const updates:any = []
userList.value.forEach((d: any) => {
if(has.includes(d.ip)){
updates.push({
key : d.id,
changes : {
isOnline : true,
updatedAt:Date.now()
}
})
}
});
await db.table('chatuser').bulkUpdate(updates)
}
if(nothas.length > 0) {
nothas.forEach((d:any) => {
d.isOnline = true
d.username = data.hostname
d.createdAt = Date.now()
d.updatedAt = Date.now()
})
await db.table('chatuser').bulkAdd(nothas)
}
await getUserList()
}
const addFile = async (data:any) => {
const targetUser:any = await getTargetUser(data)
const files:any = []
data.fileList.forEach((d:any) => {
d.save_path = d.save_path.replace(/\\/g, "/");
files.push({
name : d.name,
path : d.save_path,
ext : d.save_path.split('.').pop(),
content: d.content
})
})
const saveMsg: any = {
type: 'file',
targetId: targetUser.id,
targetIp: targetUser.ip,
content: files,
reciperInfo: data.senderInfo,
createdAt: Date.now(),
isMe: false,
isRead: false,
status: 'reciped'
}
if (targetUser.id === chatTargetId.value) {
saveMsg.readAt = Date.now()
saveMsg.isRead = true
msgList.value.push(saveMsg)
}
console.log(saveMsg)
await db.addOne('chatmsg', saveMsg)
//await getMsgList()
await updateContentList(saveMsg)
if (config.storeType === 'browser') {
await storeFile(files)
}
handleSelect(1)
}
const storeFile = async(fileList : any) => {
if (fileList.length < 1) return;
console.log(fileList)
for (let i = 0; i < fileList.length; i++) {
let content = fileList[i].content
if(typeof content === 'string') {
if(isBase64(content)){
content = base64ToBuffer(content);
}
const path = dirname(fileList[i].path)
//console.log(path)
await sys?.fs.mkdir(path);
await sys?.fs.writeFile(fileList[i].path, content);
}
}
}
const getTargetUser = async (data:any) => {
let targetUser:any = userList.value.find((d: any) => d.ip === data.senderInfo.ip)
if (!targetUser){
targetUser = {
isOnline : true,
ip:data.senderInfo.ip,
hostname: data.senderInfo.hostname,
username : data.senderInfo.hostname,
createdAt : Date.now(),
updatedAt : Date.now()
}
targetUser.id = await db.addOne("chatuser",targetUser)
userList.value.unshift(targetUser)
}
return targetUser
}
const addText = async (data:any) => {
const targetUser:any = await getTargetUser(data)
const saveMsg:any = {
type: 'text',
targetId: targetUser.id,
targetIp: targetUser.ip,
content: data.content,
reciperInfo: data.senderInfo,
createdAt: Date.now(),
isMe: false,
isRead: false,
status: 'reciped'
}
if (targetUser.id === chatTargetId.value){
saveMsg.readAt = Date.now()
saveMsg.isRead = true
msgList.value.push(saveMsg)
}
//console.log(saveMsg)
await db.addOne('chatmsg', saveMsg)
//await getMsgList()
await updateContentList(saveMsg)
handleSelect(1)
}
const sendMsg = async () => {
if(chatTargetId.value < 1) {
return
}
const saveMsg:any = {
type : 'text',
targetId : chatTargetId.value,
targetIp: chatTargetIp.value,
content: sendInfo.value.trim(),
senderInfo: toRaw(hostInfo.value),
createdAt:Date.now(),
isMe:true,
isRead:false,
status:'sending'
}
//console.log(saveMsg)
const msgId = await db.addOne('chatmsg', saveMsg)
//await getMsgList()
msgList.value.push(saveMsg)
const targetUser = userList.value.find((d: any) => d.id === chatTargetId.value)
//console.log(targetUser)
if(targetUser.isOnline) {
const postUrl = `http://${targetUser.ip}:56780/message`
const completion = await fetch(postUrl, {
method: "POST",
body: JSON.stringify(saveMsg),
})
if (!completion.ok) {
console.log(completion)
}else{
saveMsg.isRead = true
saveMsg.status = 'sended'
saveMsg.readAt = Date.now()
await db.update('chatmsg', msgId, saveMsg)
}
}
sendInfo.value = ""
await updateContentList(saveMsg)
}
//上传文件资源
async function uploadFile(paths:any) {
if (chatTargetId.value < 1) {
return
}
const targetUser = userList.value.find((d: any) => d.id === chatTargetId.value)
if (!targetUser.isOnline) {
notifyError("The user is not online!");
return;
}
if (!hostInfo.value || !hostInfo.value.ip) {
notifyError("Please wait for a moment");
return;
}
//console.log(paths)
const formData = new FormData();
const errstr:any = []
const files:any = []
for (let i = 0; i < paths.length; i++) {
const content = await sys?.fs.readFile(paths[i]);
let blobContent;
if(!content || content == ''){
errstr.push(paths[i] + " is empty")
continue
}
if (content instanceof ArrayBuffer) {
blobContent = new Blob([content]);
}
else if (typeof content === 'string') {
if (isBase64(content)) {
const base64 = base64ToBuffer(content);
blobContent = new Blob([base64]);
} else {
blobContent = new Blob([content], { type: "text/plain;charset=utf-8" });
}
}
else {
errstr.push(paths[i] + " type is error")
continue
}
const fileName = paths[i].split("/").pop()
files.push({
name: fileName,
path: paths[i],
ext : fileName.split(".").pop(),
})
//files.push(blobContent);
formData.append(`files`, blobContent, fileName);
}
if(errstr.length > 0) {
errstr.forEach((d:any) => {
notifyError(d);
})
return
}
//formData.append("files", files);
formData.append("ip", hostInfo.value.ip);
formData.append("hostname", hostInfo.value.hostname);
//console.log(formData)
const postUrl = `http://${targetUser.ip}:56780/upload`
const res = await fetch(postUrl, {
method: "POST",
body: formData,
});
if (!res.ok) {
console.log(res);
notifyError("Upload error!");
return;
}
const saveMsg: any = {
type: 'file',
targetId: targetUser.id,
targetIp: targetUser.ip,
content: files,
reciperInfo: toRaw(targetUser),
createdAt: Date.now(),
isMe: false,
isRead: true,
status: 'reciped'
}
console.log(saveMsg)
await db.addOne('chatmsg', saveMsg)
msgList.value.push(saveMsg)
notifySuccess("upload success!");
}
return {
userList,
navList,
sendInfo,
navId,
chatTargetId,
chatTargetIp,
msgList,
hostInfo,
contentList,
emojiList,
showChooseFile,
pageSize,
init,
setUserList,
getUserList,
handleSelect,
setChatId,
sendMsg,
addText,
addFile,
uploadFile,
moreMsgList,
refreshUserList,
clearMsg
}
})

View file

@ -181,20 +181,18 @@ export const appList = [
ext: ['bb'], ext: ['bb'],
eventType: "exportBaiban" eventType: "exportBaiban"
}, },
// { {
// name: 'localchat', name: 'localchat',
// appIcon: "chat", appIcon: "chat",
// content: Chat, content: "Chat",
// view: Chat, frame: true,
// frame: true, width: 800,
// width: 800, height: 600,
// height: 600, center: true,
// center: true, resizable: true,
// resizable: true, isDeskTop: true,
// isDeskTop: true, isMagnet: false,
// isMagnet: false, },
// ext: []
// },
{ {
name: 'piceditor', name: 'piceditor',

View file

@ -24,7 +24,7 @@ export const getSystemConfig = (ifset = false) => {
} }
// 初始化API相关URL若本地存储中已存在则不进行覆盖 // 初始化API相关URL若本地存储中已存在则不进行覆盖
if (!config.apiUrl) { if (!config.apiUrl) {
config.apiUrl = 'http://localhost:56710'; config.apiUrl = 'http://localhost:56780';
} }
// 初始化用户信息,若本地存储中已存在则不进行覆盖 // 初始化用户信息,若本地存储中已存在则不进行覆盖

111
frontend/src/util/update.ts Normal file
View file

@ -0,0 +1,111 @@
//import { EventsOff, EventsOn } from '~/runtime';
import manifest from '../../package.json';
export async function checkUpdate() {
if(!(window as any).go) return;
const updateGiteeUrl = `https://gitee.com/api/v5/repos/ruitao_admin/godoos/releases/`
const releaseRes = await fetch(updateGiteeUrl)
if(!releaseRes.ok) return;
const releaseData = await releaseRes.json()
const versionTag = releaseData.tag_name;
if(!versionTag) return;
if (versionTag.replace('v', '') <= manifest.version) return;
const verifyUrl = `${updateGiteeUrl}tags/${versionTag}`;
const verRes = await fetch(verifyUrl);
if(!verRes.ok) return;
const verData = await verRes.json()
if(!verData.assets || verData.assets.length <= 0) return;
const updateUrl = `${updateGiteeUrl}download/${versionTag}/${asset.name}`;
fetch(`${updateGiteeUrl}latest`).then((r) => {
if (r.ok) {
r.json().then((data) => {
if (data.tag_name) {
// const versionTag = data.tag_name;
// console.log(versionTag)
// if (versionTag.replace('v', '') > manifest.version) {
// const verifyUrl = `${updateGiteeUrl}tags/${versionTag}`;
// }
/*
if (versionTag.replace('v', '') > manifest.version) {
const verifyUrl = `${updateGiteeUrl}tags/${versionTag}`;
fetch(verifyUrl).then((r) => {
if (r.ok) {
r.json().then((data) => {
if (data.assets && data.assets.length > 0) {
const asset = data.assets.find((a: any) => a.name.toLowerCase().includes(commonStore.platform.toLowerCase().replace('darwin', 'macos')));
if (asset) {
const updateUrl = `${updateGiteeUrl}download/${versionTag}/${asset.name}`;
toastWithButton(t('New Version Available') + ': ' + versionTag, t('Update'), () => {
DeleteFile('cache.json');
const progressId = 'update_app';
const progressEvent = 'updateApp';
const updateProgress = (ds: DownloadStatus | null) => {
const content =
t('Downloading update, please wait. If it is not completed, please manually download the program from GitHub and replace the original program.')
+ (ds ? ` (${ds.progress.toFixed(2)}% ${bytesToReadable(ds.transferred)}/${bytesToReadable(ds.size)})` : '');
const options: ToastOptions = {
type: 'info',
position: 'bottom-left',
autoClose: false,
toastId: progressId,
hideProgressBar: false,
progress: ds ? ds.progress / 100 : 0
};
if (toast.isActive(progressId))
toast.update(progressId, {
render: content,
...options
});
else
toast(content, options);
};
updateProgress(null);
EventsOn(progressEvent, updateProgress);
UpdateApp(updateUrl).then(() => {
toast(t('Update completed, please restart the program.'), {
type: 'success',
position: 'bottom-left',
autoClose: false
}
);
}).catch((e) => {
toast(t('Update Error') + ' - ' + (e.message || e), {
type: 'error',
position: 'bottom-left',
autoClose: false
});
}).finally(() => {
toast.dismiss(progressId);
EventsOff(progressEvent);
});
}, {
autoClose: false,
position: 'bottom-left'
});
}
}
});
} else {
throw new Error('Verify response was not ok.');
}
});
} else {
if (notifyEvenLatest) {
toast(t('This is the latest version'), { type: 'success', position: 'bottom-left', autoClose: 2000 });
}
}
*/
} else {
throw new Error('Invalid response.');
}
});
} else {
throw new Error('Network response was not ok.');
}
}
).catch((e) => {
//toast(t('Updates Check Error') + ' - ' + (e.message || e), { type: 'error', position: 'bottom-left' });
});
}

View file

@ -34,10 +34,11 @@
"baseUrl": "./", "baseUrl": "./",
"outDir": "./", "outDir": "./",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"],
"~/*": ["./wailsjs/*"],
} }
}, },
"include": ["src/**/*.ts","src/**/**/*.ts", "src/**/*", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "include": ["src/**/*.ts","wailsjs/**/*.ts","wailsjs/**/*.js","src/**/**/*.ts", "src/**/*", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": [ "exclude": [
"dist", "dist",
"node_modules" "node_modules"

View file

@ -28,7 +28,8 @@ export default defineConfig(async () => ({
}, },
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, 'src') "@": path.resolve(__dirname, 'src'),
"~": path.resolve(__dirname, 'wailsjs')
}, },
}, },
build:{ build:{

3
go.mod
View file

@ -5,12 +5,15 @@ go 1.21
toolchain go1.22.5 toolchain go1.22.5
require ( require (
github.com/cavaliergopher/grab/v3 v3.0.1
github.com/fsnotify/fsnotify v1.4.9 github.com/fsnotify/fsnotify v1.4.9
github.com/minio/selfupdate v0.6.0
github.com/shirou/gopsutil v2.21.11+incompatible github.com/shirou/gopsutil v2.21.11+incompatible
github.com/wailsapp/wails/v2 v2.9.1 github.com/wailsapp/wails/v2 v2.9.1
) )
require ( require (
aead.dev/minisign v0.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect github.com/tklauser/numcpus v0.8.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect

17
go.sum
View file

@ -1,5 +1,9 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -39,6 +43,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -75,18 +81,27 @@ github.com/wailsapp/wails/v2 v2.9.1 h1:irsXnoQrCpeKzKTYZ2SUVlRRyeMR6I0vCO9Q1cvlE
github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI= github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -95,7 +110,9 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

142
localchat/client.go Normal file
View file

@ -0,0 +1,142 @@
package localchat
import (
"encoding/json"
"fmt"
"log"
"net"
"os"
"time"
)
// func StartServiceDiscovery() {
// conn, err := net.ListenPacket("udp", broadcastAddr)
// if err != nil {
// fmt.Println(err)
// return
// }
// defer conn.Close()
// buffer := make([]byte, 1024)
// for {
// n, addr, err := conn.ReadFrom(buffer)
// if err != nil {
// fmt.Println(err)
// continue
// }
// fmt.Printf("Received message: %s from %s\n", buffer[:n], addr)
// var udpMsg UdpMessage
// err = json.Unmarshal(buffer[:n], &udpMsg)
// if err != nil {
// fmt.Printf("Error unmarshalling JSON: %v\n", err)
// continue
// }
// log.Printf("Get message: %+v", udpMsg)
// OnlineUsers[udpMsg.IP] = udpMsg
// }
// }
func DiscoverServers() {
broadcastTicker := time.NewTicker(broadcartTime)
done := make(chan struct{}) // New channel to signal when to stop
go func() {
for {
select {
case <-broadcastTicker.C:
conn, err := net.Dial("udp", broadcastAddr)
if err != nil {
fmt.Println(err)
continue
}
myIP, myHostname, err := getMyIPAndHostname()
if err != nil {
log.Printf("Failed to get my IP and hostname: %v", err)
conn.Close()
continue
}
msg := UdpMessage{
Type: "online",
Hostname: myHostname,
IP: myIP,
Message: time.Now().Format("2006-01-02 15:04:05"),
}
jsonData, err := json.Marshal(msg)
if err != nil {
log.Printf("Failed to marshal message to JSON: %v", err)
conn.Close()
continue
}
log.Printf("Sending message: %+v", msg)
_, err = conn.Write(jsonData)
if err != nil {
fmt.Println(err)
}
conn.Close() // Close the connection after use
case <-done: // Signal to stop
broadcastTicker.Stop()
return
}
}
}()
// You might have some condition here to decide when to stop the ticker
// For example, a signal handling mechanism or a specific duration.
// After that condition is met, you would call:
// close(done)
}
// 获取自己的IP地址和主机名
func getMyIPAndHostname() (string, string, error) {
hostname, err := os.Hostname()
if err != nil {
return "", "", fmt.Errorf("failed to get hostname: %w", err)
}
addrs, err := net.Interfaces()
if err != nil {
return "", "", fmt.Errorf("failed to get network interfaces: %w", err)
}
var preferredIP net.IP
for _, iface := range addrs {
if iface.Flags&net.FlagUp == 0 {
// Skip interfaces that are not up
continue
}
ifAddrs, err := iface.Addrs()
if err != nil {
continue // Ignore this interface if we can't get its addresses
}
for _, addr := range ifAddrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
default:
continue
}
if ip.IsLoopback() {
continue // Skip loopback addresses
}
if ip.To4() != nil && (ip.IsPrivate() || ip.IsGlobalUnicast()) {
// Prefer global unicast or private addresses over link-local
preferredIP = ip
break
}
}
if preferredIP != nil {
// Found a preferred IP, break out of the loop
break
}
}
if preferredIP == nil {
return "", "", fmt.Errorf("no preferred non-loopback IPv4 address found")
}
return preferredIP.String(), hostname, nil
}

44
localchat/file.go Normal file
View file

@ -0,0 +1,44 @@
package localchat
import (
"fmt"
"log"
"os"
"path/filepath"
"time"
)
func GetChatPath() (string, error) {
baseDir, err := getAppDir()
if err != nil {
return "", err
}
modelDir := filepath.Join(baseDir, "C", "Users", "Reciv", time.Now().Format("2006-01-02"))
if !pathExists(modelDir) {
os.MkdirAll(modelDir, 0755)
}
return modelDir, nil
}
func getAppDir() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
return filepath.Join(homeDir, ".godoos", "os"), nil
}
func pathExists(dir string) bool {
_, err := os.Stat(dir)
if err == nil {
//log.Println("文件夹存在")
return true
} else if os.IsNotExist(err) {
//log.Println("文件夹不存在")
return false
} else if os.IsExist(err) {
//log.Println("文件夹存在")
return true
} else {
log.Println("发生错误:", err)
return false
}
}

84
localchat/server.go Normal file
View file

@ -0,0 +1,84 @@
//go:build !windows
// +build !windows
package localchat
import (
"encoding/json"
"fmt"
"log"
"net"
)
func StartServiceDiscovery() {
conn, err := net.ListenPacket("udp", broadcastAddr)
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
buffer := make([]byte, 1024)
for {
n, addr, err := conn.ReadFrom(buffer)
if err != nil {
fmt.Println(err)
continue
}
fmt.Printf("Received message: %s from %s\n", buffer[:n], addr)
var udpMsg UdpMessage
err = json.Unmarshal(buffer[:n], &udpMsg)
if err != nil {
fmt.Printf("Error unmarshalling JSON: %v\n", err)
continue
}
log.Printf("Get message: %+v", udpMsg)
OnlineUsers[udpMsg.IP] = udpMsg
}
}
/*
func StartServiceDiscovery(port int, clientPort int) {
addr := fmt.Sprintf(":%d", port)
serviceAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
log.Fatalf("Failed to resolve UDP address: %v", err)
}
conn, err := net.ListenUDP("udp", serviceAddr)
if err != nil {
log.Fatalf("Failed to listen on UDP: %v", err)
}
defer conn.Close()
buffer := make([]byte, 1024)
hostname, err := os.Hostname()
if err != nil {
log.Fatalf("Failed to get hostname: %v", err)
}
for {
// 监听UDP广播请求
_, _, err := conn.ReadFromUDP(buffer)
if err != nil {
log.Printf("Error reading from UDP: %v", err)
continue
}
// 构建广播消息包含IP和主机名
ip, _, _ := net.SplitHostPort(serviceAddr.String())
response := fmt.Sprintf("Chat Server is running at IP: %s, Hostname: %s", ip, hostname)
broadcastMessage(conn, response, clientPort)
}
}
func broadcastMessage(conn *net.UDPConn, message string, clientPort int) {
// 广播给所有客户端
broadcastAddr := &net.UDPAddr{IP: net.ParseIP("255.255.255.255"), Port: clientPort}
_, err := conn.WriteToUDP([]byte(message), broadcastAddr)
if err != nil {
log.Printf("Failed to broadcast message: %v", err)
}
}
*/

112
localchat/sse.go Normal file
View file

@ -0,0 +1,112 @@
package localchat
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
func init() {
go StartServiceDiscovery()
go DiscoverServers()
}
func SseHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.WriteHeader(http.StatusOK)
ticker := time.NewTicker(broadcartTime) // 每3秒检查一次在线用户
defer ticker.Stop()
// 处理新消息
ctx := r.Context()
// 使用Context来监听请求的取消
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered in SSE goroutine: %v", r)
}
}()
for {
select {
case <-ctx.Done(): // 当请求被取消时,退出循环
return
case msg := <-messageChan:
// 构造JSON数据
jsonData, err := json.Marshal(msg)
if err != nil {
log.Printf("Failed to marshal message to JSON: %v", err)
continue
}
// 通过SSE发送JSON数据
fmt.Fprintf(w, "data: %s\n\n", string(jsonData))
flusher.Flush()
}
}
}()
myIP, myHostname, err := getMyIPAndHostname()
if err != nil {
log.Printf("Failed to get my IP and hostname: %v", err)
return
}
for {
select {
case <-ticker.C: // 每隔一段时间检查并广播在线用户
var userList []UdpMessage
// 首先将自己的IP和主机名放入列表
myMsg := UdpMessage{
IP: myIP,
Hostname: myHostname,
Type: "online",
Message: time.Now().Format("2006-01-02 15:04:05"),
}
userList = append(userList, myMsg)
log.Printf("Online users: %v", OnlineUsers)
for ip, info := range OnlineUsers {
if ip != myIP { // 确保不重复添加自己
userList = append(userList, info)
}
}
res := UserList{Type: "user_list", Content: userList}
// 将用户列表转换为JSON字符串
jsonData, err := json.Marshal(res)
if err != nil {
log.Printf("Failed to marshal online users to JSON: %v", err)
continue
}
// 通过SSE发送JSON数据
fmt.Fprintf(w, "data: %s\n\n", string(jsonData))
flusher.Flush()
}
}
}
func HandleMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
var msg Message
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&msg); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// 将消息放入messageChan
messageChan <- msg
log.Printf("Received text message from %s: %s", msg.SenderInfo.IP, msg.Content)
// 这里可以添加存储文本消息到数据库或其他处理逻辑
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Text message send successfully")
}

61
localchat/type.go Normal file
View file

@ -0,0 +1,61 @@
package localchat
import (
"sync"
"time"
)
// type SenderInfo struct {
// SenderIP string `json:"sender_ip"`
// Username string `json:"username"`
// }
type Message struct {
Type string `json:"type"` // 消息类型,如"text"、"image"等
Content string `json:"content"` // 消息内容
SenderInfo UserInfo `json:"senderInfo"` // 发送者的IP地址
FileInfo FilePartInfo `json:"fileInfo"`
FileList []UploadInfo `json:"fileList"`
}
type UserInfo struct {
IP string `json:"ip"`
Hostname string `json:"hostname"`
}
type UdpMessage struct {
Type string `json:"type"`
IP string `json:"ip"`
Hostname string `json:"hostname"`
Message string `json:"message"`
}
type UserList struct {
Type string `json:"type"`
Content []UdpMessage `json:"content"`
}
type UploadInfo struct {
Name string `json:"name"`
SavePath string `json:"save_path"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
// 文件分片信息
type FilePartInfo struct {
FileName string `json:"filename"`
PartNumber int `json:"part_number"`
TotalParts int `json:"total_parts"`
}
// 分片上传状态跟踪
type UploadStatus struct {
sync.Mutex
Status map[string]int // key: fileName, value: 已上传分片数
}
var (
messageChan = make(chan Message, 100) // 缓存大小根据实际情况设定
)
var uploadStatus = UploadStatus{Status: make(map[string]int)}
var broadcartTime = 3 * time.Second
var broadcastAddr = "224.0.0.1:1679" // 多播地址
// var broadcastAddr = "255.255.255.255:1769" // 广播地址
var OnlineUsers = make(map[string]UdpMessage) // 全局mapkey为IPvalue为主机名

204
localchat/upload.go Normal file
View file

@ -0,0 +1,204 @@
package localchat
import (
"encoding/base64"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// 合并文件分片
func mergeFiles(fileName string, totalParts int, chatDir string) error {
var parts []io.Reader
for i := 1; i <= totalParts; i++ {
filePath := fmt.Sprintf("%v%v_%v.part", chatDir, fileName, i)
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open part %d: %w", i, err)
}
defer file.Close()
parts = append(parts, file)
}
mergedFilePath := fmt.Sprintf("%vmerged_%v", chatDir, fileName)
mergedFile, err := os.Create(mergedFilePath)
if err != nil {
return fmt.Errorf("failed to create merged file: %w", err)
}
defer mergedFile.Close()
_, err = io.Copy(mergedFile, io.MultiReader(parts...))
if err != nil {
return fmt.Errorf("failed to merge files: %w", err)
}
// 合并后清理分片文件
for i := 1; i <= totalParts; i++ {
os.Remove(fmt.Sprintf("%v%v_%v.part", chatDir, fileName, i))
}
return nil
}
func UploadBigFileHandler(w http.ResponseWriter, r *http.Request, msg Message) {
chatDir, err := GetChatPath()
if err != nil {
http.Error(w, "Failed to get chat path", http.StatusInternalServerError)
return
}
// 创建或打开临时文件以写入分片
tempFilePath := fmt.Sprintf("%v%v_%v.part", chatDir, msg.FileInfo.FileName, msg.FileInfo.PartNumber)
out, err := os.Create(tempFilePath)
if err != nil {
log.Printf("Failed to create temp file: %v", err)
http.Error(w, "Failed to create temp file", http.StatusInternalServerError)
return
}
defer out.Close()
// 将请求体的内容写入临时文件
_, err = io.Copy(out, r.Body)
if err != nil {
log.Printf("Failed to write file part: %v", err)
http.Error(w, "Failed to write file part", http.StatusInternalServerError)
return
}
// 更新上传状态
uploadStatus.Lock()
uploadStatus.Status[msg.FileInfo.FileName]++
if uploadStatus.Status[msg.FileInfo.FileName] == msg.FileInfo.TotalParts {
// 所有分片上传完成,触发合并
go func() {
err := mergeFiles(msg.FileInfo.FileName, msg.FileInfo.TotalParts, chatDir)
if err != nil {
log.Printf("Failed to merge files for %v: %v", msg.FileInfo.FileName, err)
} else {
log.Printf("Merged file %v successfully", msg.FileInfo.FileName)
}
// 清理状态记录
delete(uploadStatus.Status, msg.FileInfo.FileName)
msg.Content = "uploaded"
messageChan <- msg
}()
}
uploadStatus.Unlock()
// 返回成功响应
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "File part uploaded successfully")
}
// SaveContentToFile 保存内容到文件并返回UploadInfo结构体
func SaveContentToFile(content, fileName string) (UploadInfo, error) {
uploadBaseDir, err := GetChatPath()
if err != nil {
return UploadInfo{}, err
}
appDir, err := getAppDir()
if err != nil {
return UploadInfo{}, err
}
// 去除文件名中的空格
fileNameWithoutSpaces := strings.ReplaceAll(fileName, " ", "_")
fileNameWithoutSpaces = strings.ReplaceAll(fileNameWithoutSpaces, "/", "")
fileNameWithoutSpaces = strings.ReplaceAll(fileNameWithoutSpaces, `\`, "")
// 提取文件名和扩展名
// 查找最后一个点的位置
lastDotIndex := strings.LastIndexByte(fileNameWithoutSpaces, '.')
// 如果找到点,则提取扩展名,否则视为没有扩展名
ext := ""
if lastDotIndex != -1 {
ext = fileNameWithoutSpaces[lastDotIndex:]
fileNameWithoutSpaces = fileNameWithoutSpaces[:lastDotIndex]
} else {
ext = ""
}
randFileName := fmt.Sprintf("%s_%s%s", fileNameWithoutSpaces, strconv.FormatInt(time.Now().UnixNano(), 10), ext)
savePath := filepath.Join(uploadBaseDir, randFileName)
if err := os.MkdirAll(filepath.Dir(savePath), 0755); err != nil {
return UploadInfo{}, err
}
if err := os.WriteFile(savePath, []byte(content), 0644); err != nil {
return UploadInfo{}, err
}
content = string(content)
// 检查文件内容是否以"link::"开头
if !strings.HasPrefix(content, "link::") {
content = base64.StdEncoding.EncodeToString([]byte(content))
}
return UploadInfo{
Name: fileNameWithoutSpaces,
SavePath: strings.TrimPrefix(savePath, appDir),
Content: content,
CreatedAt: time.Now(),
}, nil
}
// MultiUploadHandler 处理多文件上传请求
func MultiUploadHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(10000 << 20); err != nil {
http.Error(w, "Failed to parse multipart form", http.StatusBadRequest)
return
}
files := r.MultipartForm.File["files"]
if len(files) == 0 {
http.Error(w, "No file parts in the request", http.StatusBadRequest)
return
}
fileInfoList := make([]UploadInfo, 0, len(files))
for _, fileHeader := range files {
file, err := fileHeader.Open()
if err != nil {
http.Error(w, "Failed to open uploaded file", http.StatusBadRequest)
continue
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
http.Error(w, "Failed to read uploaded file", http.StatusBadRequest)
continue
}
//log.Printf(string(content))
// 保存上传的文件内容
info, err := SaveContentToFile(string(content), fileHeader.Filename)
if err != nil {
http.Error(w, "Failed to save uploaded file", http.StatusBadRequest)
continue
}
log.Println(info.SavePath)
//info.SavePath = savePath
fileInfoList = append(fileInfoList, info)
}
user := UserInfo{
IP: r.FormValue("ip"),
Hostname: r.FormValue("hostname"),
}
msg := Message{
Type: "file",
Content: "file recieved",
SenderInfo: user,
FileList: fileInfoList,
}
messageChan <- msg
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "File send successfully")
//serv.Res(serv.Response{Code: 0, Data: fileInfoList}, w)
}

117
localchat/windiscovery.go Normal file
View file

@ -0,0 +1,117 @@
//go:build windows
// +build windows
package localchat
import (
"encoding/json"
"fmt"
"log"
"net"
)
func StartServiceDiscovery() {
// 解析多播地址
addr, err := net.ResolveUDPAddr("udp4", broadcastAddr)
if err != nil {
fmt.Println("Error resolving multicast address:", err)
return
}
// 监听本地网络接口上的多播地址
conn, err := net.ListenMulticastUDP("udp4", nil, addr)
if err != nil {
fmt.Println("Error listening on multicast address:", err)
return
}
defer conn.Close()
// 设置组播通信的网路接口
// intf,err := net.InterfaceByName("eth0")
// if err != nil {
// fmt.Println("Error getting network interface:", err)
// return
// }
// conn.setMulticastInterface(intf)
buffer := make([]byte, 1024)
for {
n, addr, err := conn.ReadFromUDP(buffer)
if err != nil {
log.Printf("Error reading from UDP: %v", err)
continue
}
fmt.Printf("Received message: %s from %s\n", buffer[:n], addr)
var udpMsg UdpMessage
err = json.Unmarshal(buffer[:n], &udpMsg)
if err != nil {
fmt.Printf("Error unmarshalling JSON: %v\n", err)
continue
}
log.Printf("Get message: %+v", udpMsg)
OnlineUsers[udpMsg.IP] = udpMsg
}
}
/*
// findMulticastInterface 查找适合多播的网络接口
func findMulticastInterface() (*net.Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for _, iface := range ifaces {
if strings.HasPrefix(iface.Name, "eth") || strings.HasPrefix(iface.Name, "en") || strings.HasPrefix(iface.Name, "wlan") {
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
}
if ip != nil && !ip.IsLoopback() {
return &iface, nil
}
}
}
}
return nil, fmt.Errorf("no suitable multicast interface found")
}
// setupWindowsMulticast 为Windows平台设置多播
func setupWindowsMulticast(conn net.PacketConn, iface *net.Interface, multicastIP net.IP) error {
// 获取文件描述符
if sock, err := conn.(*net.UDPConn).File(); err != nil {
return err
} else {
defer sock.Close()
fd := sock.Fd()
// 加入多播组
var mreq windows.RawSockaddrInet4
mreq.Family = windows.AF_INET
mreq.Port = 0
// 确保 multicastIP 是 IPv4 并转换为 [4]byte
ipv4 := multicastIP.To4()
if ipv4 == nil {
return errors.New("multicast IP is not an IPv4 address")
}
copy(mreq.Addr[:], ipv4)
if err := windows.SetsockoptIPv4MulticastInterface(fd, windows.IPMULTICAST_IF, &mreq); err != nil {
return err
}
// 绑定到特定接口(可选,根据需求调整)
ifaceIndex := uint32(iface.Index)
if err := windows.BindToDevice(fd, windows.StringToUTF16Ptr(fmt.Sprintf("%d", ifaceIndex))); err != nil {
return err
}
return nil
}
}*/

View file

@ -3,6 +3,8 @@ package main
import ( import (
"embed" "embed"
App "godoos/app"
"github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/assetserver"
@ -13,7 +15,7 @@ var assets embed.FS
func main() { func main() {
// Create an instance of the app structure // Create an instance of the app structure
app := NewApp() app := App.NewApp()
// Create application with options // Create application with options
err := wails.Run(&options.App{ err := wails.Run(&options.App{
@ -24,8 +26,8 @@ func main() {
Assets: assets, Assets: assets,
}, },
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup, OnStartup: app.Startup,
//OnShutdown: app.shutdown, OnShutdown: app.Shutdown,
Bind: []interface{}{ Bind: []interface{}{
app, app,
}, },