that's a lot of code
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,7 +26,7 @@ go.work.sum
|
|||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
pi.json
|
||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
54
README.md
54
README.md
@@ -1,7 +1,57 @@
|
|||||||
# pi
|
# π
|
||||||
|
|
||||||
Experimental and extremely weird XMPP client made with Go. No solicitors.
|
Experimental and extremely weird XMPP client made with Go. No solicitors.
|
||||||
|
|
||||||
pi is currently pre-pre-pre-pre alpha software which you should not use right now.
|
pi is currently pre-pre-pre-pre alpha software which you should not use right now.
|
||||||
|
|
||||||
pi uses [Fyne](https://fyne.io) for the frontend.
|
pi uses [Fyne](https://fyne.io) for the frontend and uses the [Oasis SDK](https://github.com/jjj333-p/oasis-sdk) for XMPP functionality.
|
||||||
|
|
||||||
|
pi is an extremely opinionated client. It aims to have as little extra windows as possible, instead using alt-menus to perform many of the actions you'd see in a typical client.
|
||||||
|
|
||||||
|
|
||||||
|
## διαμόρφωση
|
||||||
|
(configuration)
|
||||||
|
|
||||||
|
In order to use pi, you currently have to create a `pi.json` file in the working directory of the executable. Here is how one looks like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Host":"example.com:5222",
|
||||||
|
"User":"user@example.com",
|
||||||
|
"Password":"123456",
|
||||||
|
"DisplayName":"user",
|
||||||
|
"NoTLS":false,
|
||||||
|
"StartTLS":true,
|
||||||
|
"Mucs":["room@muc.example.com"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit this file as necessary.
|
||||||
|
|
||||||
|
Currently joining and saving DM tabs is not supported, nor is getting avatars, reactions, encryption of media embed.
|
||||||
|
|
||||||
|
As of writing, pi supports basic message sending and receiving, replies and file upload.
|
||||||
|
|
||||||
|
|
||||||
|
## να χτίσω
|
||||||
|
(building)
|
||||||
|
|
||||||
|
To build pi, you will need the latest version of Go, at least 1.21. You can grab it [here](https://go.dev).
|
||||||
|
|
||||||
|
The build instructions are very simple. Simply clone the repo, fetch the repositories and build the program:
|
||||||
|
|
||||||
|
Here is a summary of the commands you would need to use to build and run the program:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/sunglocto/pi
|
||||||
|
cd pi
|
||||||
|
go mod tidy
|
||||||
|
go build .
|
||||||
|
vim pi.json
|
||||||
|
./pi
|
||||||
|
```
|
||||||
|
|
||||||
|
Static executable snapshots are also provided for GNU/Linux systems.
|
||||||
|
|
||||||
|
## χρήση
|
||||||
|
(usage)
|
||||||
|
|
||||||
|
TODO
|
||||||
|
4
go.mod
4
go.mod
@@ -4,6 +4,8 @@ go 1.24.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.6.2
|
fyne.io/fyne/v2 v2.6.2
|
||||||
|
fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb
|
||||||
|
github.com/mbaklor/fyne-catppuccin v0.0.2
|
||||||
mellium.im/xmpp v0.22.0
|
mellium.im/xmpp v0.22.0
|
||||||
pain.agency/oasis-sdk v0.0.0-20250803100711-2ed1355344d4
|
pain.agency/oasis-sdk v0.0.0-20250803100711-2ed1355344d4
|
||||||
)
|
)
|
||||||
@@ -11,6 +13,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
fyne.io/systray v1.11.0 // indirect
|
fyne.io/systray v1.11.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||||
|
github.com/catppuccin/go v0.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fredbi/uri v1.1.0 // indirect
|
github.com/fredbi/uri v1.1.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
@@ -28,7 +31,6 @@ require (
|
|||||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
7
go.sum
7
go.sum
@@ -2,9 +2,12 @@ fyne.io/fyne/v2 v2.6.2 h1:RPgwmXWn+EuP/TKwO7w5p73ILVC26qHD9j3CZUZNwgM=
|
|||||||
fyne.io/fyne/v2 v2.6.2/go.mod h1:9IJ8uWgzfcMossFoUkLiOrUIEtaDvF4nML114WiCtXU=
|
fyne.io/fyne/v2 v2.6.2/go.mod h1:9IJ8uWgzfcMossFoUkLiOrUIEtaDvF4nML114WiCtXU=
|
||||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||||
|
fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb h1:2BazNmb/kwgqRdvE9L+NgW8sfoWGn3iy1Ox8R4+CSmc=
|
||||||
|
fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb/go.mod h1:u3LF1EkElytjOT8OHxft16trctGndF9qpsoH6YIDOUU=
|
||||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||||
|
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
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=
|
||||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||||
@@ -47,6 +50,8 @@ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe9
|
|||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mbaklor/fyne-catppuccin v0.0.2 h1:yMNnYkmFwjKJkFQvCd1uNKZDs07ZC85wTkedTIGcViE=
|
||||||
|
github.com/mbaklor/fyne-catppuccin v0.0.2/go.mod h1:ZBIy4dV1yMj+7oEaZYkXm5OfYESmXuPWwNcuUmD1Njo=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||||
|
448
main.go
448
main.go
@@ -1,58 +1,244 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
_ "net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
"fyne.io/fyne/v2/canvas"
|
_ "fyne.io/fyne/v2/canvas"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/layout"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
_ "fyne.io/x/fyne/theme"
|
||||||
|
catppuccin "github.com/mbaklor/fyne-catppuccin"
|
||||||
"mellium.im/xmpp/jid"
|
"mellium.im/xmpp/jid"
|
||||||
"mellium.im/xmpp/muc"
|
"mellium.im/xmpp/muc"
|
||||||
|
"mellium.im/xmpp/stanza"
|
||||||
oasisSdk "pain.agency/oasis-sdk"
|
oasisSdk "pain.agency/oasis-sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// by sunglocto
|
||||||
|
// license AGPL
|
||||||
|
|
||||||
func main() {
|
type Message struct {
|
||||||
|
Author string
|
||||||
|
Content string
|
||||||
|
ID string
|
||||||
|
ReplyID string
|
||||||
|
Raw oasisSdk.XMPPChatMessage
|
||||||
|
}
|
||||||
|
|
||||||
login := oasisSdk.LoginInfo{
|
type MucTab struct {
|
||||||
Host: "sunglocto.net:5222",
|
Jid jid.JID
|
||||||
User: "bot2@sunglocto.net",
|
Nick string
|
||||||
Password: "iloverobots",
|
Messages []Message
|
||||||
DisplayName: "bot2",
|
Scroller *widget.List
|
||||||
TLSoff: false,
|
isMuc bool
|
||||||
StartTLS: true,
|
}
|
||||||
MucsToJoin: []string{"ringen@muc.isekai.rocks"},
|
|
||||||
|
var chatTabs = make(map[string]*MucTab)
|
||||||
|
var tabs *container.AppTabs
|
||||||
|
var selectedId widget.ListItemID
|
||||||
|
var replying bool = false
|
||||||
|
var notifications bool = true
|
||||||
|
|
||||||
|
type myTheme struct{}
|
||||||
|
|
||||||
|
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
|
||||||
|
return catppuccin.New().Color(name, variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
|
||||||
|
return theme.DefaultTheme().Icon(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m myTheme) Font(style fyne.TextStyle) fyne.Resource {
|
||||||
|
return theme.DefaultTheme().Font(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m myTheme) Size(name fyne.ThemeSizeName) float32 {
|
||||||
|
if name == theme.SizeNameHeadingText {
|
||||||
|
return 18
|
||||||
|
}
|
||||||
|
return theme.DefaultTheme().Size(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollDownOnNewMessage bool = true
|
||||||
|
var w fyne.Window
|
||||||
|
var a fyne.App
|
||||||
|
|
||||||
|
func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
|
||||||
|
mucJidStr := chatJid.String()
|
||||||
|
if _, ok := chatTabs[mucJidStr]; ok {
|
||||||
|
// Tab already exists
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
maina := container.New(layout.NewHBoxLayout(), widget.NewLabel("pi"))
|
tabData := &MucTab{
|
||||||
scroller := container.NewVScroll(maina)
|
Jid: chatJid,
|
||||||
|
Nick: nick,
|
||||||
|
Messages: []Message{},
|
||||||
|
isMuc: isMuc,
|
||||||
|
}
|
||||||
|
|
||||||
|
var scroller *widget.List
|
||||||
|
scroller = widget.NewList(
|
||||||
|
func() int {
|
||||||
|
return len(tabData.Messages)
|
||||||
|
},
|
||||||
|
func() fyne.CanvasObject {
|
||||||
|
author := widget.NewLabel("author")
|
||||||
|
author.TextStyle.Bold = true
|
||||||
|
content := widget.NewRichTextWithText("content")
|
||||||
|
content.Wrapping = fyne.TextWrapWord
|
||||||
|
return container.NewVBox(author, content)
|
||||||
|
},
|
||||||
|
func(i widget.ListItemID, co fyne.CanvasObject) {
|
||||||
|
vbox := co.(*fyne.Container)
|
||||||
|
author := vbox.Objects[0].(*widget.Label)
|
||||||
|
content := vbox.Objects[1].(*widget.RichText)
|
||||||
|
content.ParseMarkdown(tabData.Messages[i].Content)
|
||||||
|
if tabData.Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" {
|
||||||
|
author.SetText(fmt.Sprintf("%s ↳ %s", tabData.Messages[i].Author, jid.MustParse(tabData.Messages[i].ReplyID).Resourcepart()))
|
||||||
|
} else {
|
||||||
|
author.SetText(tabData.Messages[i].Author)
|
||||||
|
}
|
||||||
|
scroller.SetItemHeight(i, vbox.MinSize().Height)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
scroller.OnSelected = func(id widget.ListItemID) {
|
||||||
|
selectedId = id
|
||||||
|
}
|
||||||
|
tabData.Scroller = scroller
|
||||||
|
|
||||||
|
chatTabs[mucJidStr] = tabData
|
||||||
|
|
||||||
|
tabItem := container.NewTabItem(chatJid.Localpart(), scroller)
|
||||||
|
tabs.Append(tabItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
login := oasisSdk.LoginInfo{}
|
||||||
|
|
||||||
|
DMs := []string{}
|
||||||
|
|
||||||
|
bytes, err := os.ReadFile("./pi.json")
|
||||||
|
if err != nil {
|
||||||
|
a = app.New()
|
||||||
|
w = a.NewWindow("Error")
|
||||||
|
w.Resize(fyne.NewSize(500, 500))
|
||||||
|
dialog.ShowInformation("Error", fmt.Sprintf("Please make sure there is a file named pi.json in the same directory you are running this executable...\n%s", err.Error()), w)
|
||||||
|
w.ShowAndRun()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(bytes, &login)
|
||||||
|
if err != nil {
|
||||||
|
a = app.New()
|
||||||
|
w = a.NewWindow("Error")
|
||||||
|
w.Resize(fyne.NewSize(500, 500))
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
w.ShowAndRun()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
client, err := oasisSdk.CreateClient(
|
client, err := oasisSdk.CreateClient(
|
||||||
&login,
|
&login,
|
||||||
func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) {
|
func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) {
|
||||||
fyne.Do(func(){
|
fmt.Println(msg)
|
||||||
card := widget.NewCard(msg.From.String(), *msg.CleanedBody, canvas.NewCircle(color.White))
|
userJidStr := msg.From.Bare().String()
|
||||||
maina.Add(card)
|
tab, ok := chatTabs[userJidStr]
|
||||||
maina.Refresh()
|
fmt.Println(msg.From.String())
|
||||||
scroller.ScrollToBottom()
|
if ok {
|
||||||
})
|
str := *msg.CleanedBody
|
||||||
|
if notifications {
|
||||||
|
a.SendNotification(fyne.NewNotification(fmt.Sprintf("%s says", userJidStr), str))
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if strings.Contains(str, "https://") {
|
||||||
|
fmt.Println("Attempting to do URL thingy")
|
||||||
|
s := strings.Split(str, " ")
|
||||||
|
for i, v := range s {
|
||||||
|
_, err := url.Parse(v)
|
||||||
|
if err == nil {
|
||||||
|
s[i] = fmt.Sprintf("[%s](%s)", v, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str = strings.Join(s, " ")
|
||||||
|
}*/
|
||||||
|
var replyID string
|
||||||
|
if msg.Reply == nil {
|
||||||
|
replyID = "PICLIENT:UNAVAILABLE"
|
||||||
|
} else {
|
||||||
|
replyID = msg.Reply.ID
|
||||||
|
}
|
||||||
|
myMessage := Message{
|
||||||
|
Author: msg.From.Resourcepart(),
|
||||||
|
Content: str,
|
||||||
|
ID: msg.ID,
|
||||||
|
ReplyID: replyID,
|
||||||
|
Raw: *msg,
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.Messages = append(tab.Messages, myMessage)
|
||||||
|
fyne.Do(func() {
|
||||||
|
tab.Scroller.Refresh()
|
||||||
|
if scrollDownOnNewMessage {
|
||||||
|
tab.Scroller.ScrollToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
func(client *oasisSdk.XmppClient, _ *muc.Channel, msg *oasisSdk.XMPPChatMessage) {
|
func(client *oasisSdk.XmppClient, _ *muc.Channel, msg *oasisSdk.XMPPChatMessage) {
|
||||||
fyne.Do(func(){
|
mucJidStr := msg.From.Bare().String()
|
||||||
if msg.Reply != nil {
|
if tab, ok := chatTabs[mucJidStr]; ok {
|
||||||
|
|
||||||
|
str := *msg.CleanedBody
|
||||||
|
if notifications {
|
||||||
|
if strings.Contains(str, login.DisplayName) || (msg.Reply != nil && strings.Contains(msg.Reply.To, login.User)) {
|
||||||
|
a.SendNotification(fyne.NewNotification(fmt.Sprintf("Mentioned in %s", mucJidStr), str))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
card := widget.NewCard(msg.From.String(), *msg.CleanedBody, canvas.NewCircle(color.White))
|
/*
|
||||||
maina.Add(card)
|
if strings.Contains(str, "https://") {
|
||||||
maina.Refresh()
|
s := strings.Split(str, " ")
|
||||||
scroller.ScrollToBottom()
|
for i, v := range s {
|
||||||
})
|
_, err := url.Parse(v)
|
||||||
|
if err == nil {
|
||||||
|
s[i] = fmt.Sprintf("[%s](%s)", v, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str = strings.Join(s, " ")
|
||||||
|
}*/
|
||||||
|
fmt.Println(msg.ID)
|
||||||
|
var replyID string
|
||||||
|
if msg.Reply == nil {
|
||||||
|
replyID = "PICLIENT:UNAVAILABLE"
|
||||||
|
} else {
|
||||||
|
replyID = msg.Reply.To
|
||||||
|
}
|
||||||
|
myMessage := Message{
|
||||||
|
Author: msg.From.Resourcepart(),
|
||||||
|
Content: str,
|
||||||
|
ID: msg.ID,
|
||||||
|
ReplyID: replyID,
|
||||||
|
Raw: *msg,
|
||||||
|
}
|
||||||
|
tab.Messages = append(tab.Messages, myMessage)
|
||||||
|
fyne.Do(func() {
|
||||||
|
tab.Scroller.Refresh()
|
||||||
|
if scrollDownOnNewMessage {
|
||||||
|
tab.Scroller.ScrollToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
|
func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
|
||||||
//fromStr := from.String()
|
//fromStr := from.String()
|
||||||
@@ -72,48 +258,214 @@ func main() {
|
|||||||
fmt.Printf("%s has seen %s", from.String(), id)
|
fmt.Printf("%s has seen %s", from.String(), id)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Could not create client - " + err.Error())
|
log.Fatalln("Could not create client - " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err = client.Connect()
|
err = client.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Could not connect - " + err.Error())
|
log.Fatalln("Could not connect - " + err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
a := app.New()
|
a = app.New()
|
||||||
w := a.NewWindow("pi")
|
a.Settings().SetTheme(myTheme{})
|
||||||
w.Resize(fyne.NewSize(500,500))
|
w = a.NewWindow("pi")
|
||||||
|
w.Resize(fyne.NewSize(500, 500))
|
||||||
|
|
||||||
|
entry := widget.NewMultiLineEntry()
|
||||||
|
entry.SetPlaceHolder("Say something, you know you want to.")
|
||||||
|
|
||||||
|
sendbtn := widget.NewButton("Send", func() {
|
||||||
|
text := entry.Text
|
||||||
|
if tabs.Selected() == nil || tabs.Selected().Content == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeMucJid string
|
||||||
|
var isMuc bool
|
||||||
|
for jid, tabData := range chatTabs {
|
||||||
|
if tabData.Scroller == selectedScroller {
|
||||||
|
activeMucJid = jid
|
||||||
|
isMuc = tabData.isMuc
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if activeMucJid == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
//TODO: Fix message hack until jjj adds message sending
|
||||||
|
if replying {
|
||||||
|
m := chatTabs[activeMucJid].Messages[selectedId].Raw
|
||||||
|
client.ReplyToEvent(&m, text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var typ stanza.MessageType
|
||||||
|
if isMuc {
|
||||||
|
typ = stanza.GroupChatMessage
|
||||||
|
} else {
|
||||||
|
typ = stanza.ChatMessage
|
||||||
|
}
|
||||||
|
msg := oasisSdk.XMPPChatMessage{
|
||||||
|
Message: stanza.Message{
|
||||||
|
To: jid.MustParse(activeMucJid),
|
||||||
|
Type: typ,
|
||||||
|
},
|
||||||
|
ChatMessageBody: oasisSdk.ChatMessageBody{
|
||||||
|
Body: &text,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := client.Session.Encode(client.Ctx, msg)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !isMuc {
|
||||||
|
chatTabs[activeMucJid].Messages = append(chatTabs[activeMucJid].Messages, Message{
|
||||||
|
Author: "You",
|
||||||
|
Content: text,
|
||||||
|
})
|
||||||
|
fyne.Do(func() {
|
||||||
|
if scrollDownOnNewMessage {
|
||||||
|
chatTabs[activeMucJid].Scroller.ScrollToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
entry.SetText("")
|
||||||
|
})
|
||||||
|
|
||||||
mit := fyne.NewMenuItem("About pi", func() {
|
mit := fyne.NewMenuItem("About pi", func() {
|
||||||
dialog.ShowInformation("About pi", "the XMPP client from hell\npi is an experimental XMPP client\nwritten by Sunglocto in Go.", w)
|
dialog.ShowInformation("About pi", "the XMPP client from hell\n\npi is an experimental XMPP client\nwritten by Sunglocto in Go.", w)
|
||||||
|
})
|
||||||
|
|
||||||
|
mia := fyne.NewMenuItem("Configure message view", func() {
|
||||||
|
ch := widget.NewCheck("", func(b bool) {})
|
||||||
|
ch2 := widget.NewCheck("", func(b bool) {})
|
||||||
|
ch.Checked = scrollDownOnNewMessage
|
||||||
|
ch2.Checked = notifications
|
||||||
|
scrollView := widget.NewFormItem("Scroll to bottom on new message", ch)
|
||||||
|
notiView := widget.NewFormItem("Send notifications when mentioned", ch2)
|
||||||
|
items := []*widget.FormItem{
|
||||||
|
scrollView,
|
||||||
|
notiView,
|
||||||
|
}
|
||||||
|
dialog.ShowForm("Configure message view", "Apply", "Cancel", items, func(b bool) {
|
||||||
|
if b {
|
||||||
|
scrollDownOnNewMessage = ch.Checked
|
||||||
|
notifications = ch2.Checked
|
||||||
|
}
|
||||||
|
}, w)
|
||||||
|
})
|
||||||
|
|
||||||
|
mis := fyne.NewMenuItem("Clear chat window", func() {
|
||||||
|
dialog.ShowConfirm("Clear chat window", "Are you sure you want to clear the chat window?", func(b bool) {
|
||||||
|
if b {
|
||||||
|
fmt.Println("clearing chat")
|
||||||
|
}
|
||||||
|
}, w)
|
||||||
})
|
})
|
||||||
mib := fyne.NewMenuItem("Join a room", func() {
|
mib := fyne.NewMenuItem("Join a room", func() {
|
||||||
nick := widget.NewEntry()
|
nickEntry := widget.NewEntry()
|
||||||
room := widget.NewEntry()
|
nickEntry.SetText(login.DisplayName)
|
||||||
|
roomEntry := widget.NewEntry()
|
||||||
items := []*widget.FormItem{
|
items := []*widget.FormItem{
|
||||||
widget.NewFormItem("Nick", nick),
|
widget.NewFormItem("Nick", nickEntry),
|
||||||
widget.NewFormItem("MUC address", room),
|
widget.NewFormItem("MUC address", roomEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.ShowForm("Join a MUC", "Join", "Cancel", items, func(b bool) {
|
dialog.ShowForm("Join a MUC", "Join", "Cancel", items, func(b bool) {
|
||||||
if b {
|
if b {
|
||||||
fmt.Println("attempting to join MUC")
|
roomJid, err := jid.Parse(roomEntry.Text)
|
||||||
fmt.Println(nick)
|
if err != nil {
|
||||||
fmt.Println(room)
|
dialog.ShowError(err, w)
|
||||||
go func(){
|
return
|
||||||
client.MucClient.Join(client.Ctx, jid.MustParse(room.Text), client.Session, nil)
|
}
|
||||||
|
nick := nickEntry.Text
|
||||||
|
go func() {
|
||||||
|
// We probably don't need to handle the error here, if it fails the user will know
|
||||||
|
_, err := client.MucClient.Join(client.Ctx, roomJid, client.Session, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
addChatTab(true, roomJid, nick)
|
||||||
}
|
}
|
||||||
}, w)
|
}, w)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mic := fyne.NewMenuItem("Upload a file", func() {
|
||||||
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
}
|
||||||
|
bytes, err := io.ReadAll(reader)
|
||||||
|
link, err := client.UploadFileFromBytes(reader.URI().String(), bytes)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.Clipboard().SetContent(link)
|
||||||
|
dialog.ShowInformation("File successfully uploaded", link, w)
|
||||||
|
}, w)
|
||||||
|
})
|
||||||
|
|
||||||
menu_help := fyne.NewMenu("π", mit)
|
menu_help := fyne.NewMenu("π", mit)
|
||||||
menu_changeroom := fyne.NewMenu("β", mib)
|
menu_changeroom := fyne.NewMenu("β", mib, mic)
|
||||||
ma := fyne.NewMainMenu(menu_help, menu_changeroom)
|
menu_configureview := fyne.NewMenu("γ", mia, mis)
|
||||||
|
bit := fyne.NewMenuItem("Mark message as read", func() {
|
||||||
|
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var activeMucJid string
|
||||||
|
for jid, tabData := range chatTabs {
|
||||||
|
if tabData.Scroller == selectedScroller {
|
||||||
|
activeMucJid = jid
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := chatTabs[activeMucJid].Messages[selectedId].Raw
|
||||||
|
client.MarkAsRead(&m)
|
||||||
|
})
|
||||||
|
|
||||||
|
bia := fyne.NewMenuItem("Toggle replying to message", func() {
|
||||||
|
replying = !replying
|
||||||
|
})
|
||||||
|
menu_messageoptions := fyne.NewMenu("Σ", bit, bia)
|
||||||
|
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions)
|
||||||
w.SetMainMenu(ma)
|
w.SetMainMenu(ma)
|
||||||
|
|
||||||
tabs := container.NewAppTabs(container.NewTabItem("pi", widget.NewLabel("pi\nthe XMPP client from hell")), container.NewTabItem("chat", scroller))
|
tabs = container.NewAppTabs(
|
||||||
w.SetContent(tabs)
|
container.NewTabItem("τίποτα", widget.NewRichTextFromMarkdown("# No chat selected.")),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, mucJidStr := range login.MucsToJoin {
|
||||||
|
mucJid, err := jid.Parse(mucJidStr)
|
||||||
|
if err == nil {
|
||||||
|
addChatTab(true, mucJid, login.DisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, userJidStr := range DMs {
|
||||||
|
fmt.Println(userJidStr)
|
||||||
|
DMjid, err := jid.Parse(userJidStr)
|
||||||
|
if err == nil {
|
||||||
|
addChatTab(false, DMjid, login.DisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.SetContent(container.NewVSplit(tabs, container.NewHSplit(entry, sendbtn)))
|
||||||
w.ShowAndRun()
|
w.ShowAndRun()
|
||||||
}
|
}
|
||||||
|
210
main.go.copy
Normal file
210
main.go.copy
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/app"
|
||||||
|
_ "fyne.io/fyne/v2/canvas"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
_ "fyne.io/fyne/v2/layout"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
_ "github.com/mbaklor/fyne-catppuccin"
|
||||||
|
_ "fyne.io/x/fyne/theme"
|
||||||
|
"mellium.im/xmpp/jid"
|
||||||
|
"mellium.im/xmpp/muc"
|
||||||
|
"mellium.im/xmpp/stanza"
|
||||||
|
oasisSdk "pain.agency/oasis-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Author string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Messages []Message
|
||||||
|
|
||||||
|
type myTheme struct{}
|
||||||
|
|
||||||
|
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
|
||||||
|
return theme.DefaultTheme().Color(name, variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
|
||||||
|
return theme.DefaultTheme().Icon(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m myTheme) Font(style fyne.TextStyle) fyne.Resource {
|
||||||
|
return theme.DefaultTheme().Font(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m myTheme) Size(name fyne.ThemeSizeName) float32 {
|
||||||
|
if name == theme.SizeNameHeadingText {
|
||||||
|
return 18
|
||||||
|
}
|
||||||
|
return theme.DefaultTheme().Size(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollDownOnNewMessage bool = true
|
||||||
|
|
||||||
|
func addMessageToView() fyne.CanvasObject {
|
||||||
|
author := widget.NewLabel("")
|
||||||
|
author.TextStyle.Bold = true
|
||||||
|
content := widget.NewLabel("")
|
||||||
|
content.Wrapping = fyne.TextWrapWord
|
||||||
|
return container.NewVBox(author, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
login := oasisSdk.LoginInfo{
|
||||||
|
Host: "sunglocto.net:5222",
|
||||||
|
User: "bot2@sunglocto.net",
|
||||||
|
Password: "iloverobots",
|
||||||
|
DisplayName: "bot2",
|
||||||
|
TLSoff: false,
|
||||||
|
StartTLS: true,
|
||||||
|
MucsToJoin: []string{"ringen@muc.isekai.rocks"},
|
||||||
|
}
|
||||||
|
|
||||||
|
scroller := widget.NewList(
|
||||||
|
func() int {
|
||||||
|
return len(Messages)
|
||||||
|
}, func() fyne.CanvasObject {
|
||||||
|
return addMessageToView()
|
||||||
|
}, func(i widget.ListItemID, co fyne.CanvasObject) {
|
||||||
|
co.(*fyne.Container).Objects[0].(*widget.Label).SetText(Messages[i].Author)
|
||||||
|
co.(*fyne.Container).Objects[1].(*widget.Label).SetText(Messages[i].Content)
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := oasisSdk.CreateClient(
|
||||||
|
&login,
|
||||||
|
func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) {
|
||||||
|
myMessage := Message{}
|
||||||
|
myMessage.Author = msg.From.Resourcepart()
|
||||||
|
myMessage.Content = *msg.CleanedBody
|
||||||
|
Messages = append(Messages, myMessage)
|
||||||
|
if scrollDownOnNewMessage {
|
||||||
|
fyne.Do(func() {
|
||||||
|
scroller.ScrollToBottom()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func(client *oasisSdk.XmppClient, _ *muc.Channel, msg *oasisSdk.XMPPChatMessage) {
|
||||||
|
myMessage := Message{}
|
||||||
|
myMessage.Author = msg.From.Resourcepart()
|
||||||
|
myMessage.Content = *msg.CleanedBody
|
||||||
|
Messages = append(Messages, myMessage)
|
||||||
|
if scrollDownOnNewMessage {
|
||||||
|
fyne.Do(func() {
|
||||||
|
scroller.ScrollToBottom()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
|
||||||
|
//fromStr := from.String()
|
||||||
|
switch state {
|
||||||
|
case oasisSdk.ChatStateActive:
|
||||||
|
case oasisSdk.ChatStateComposing:
|
||||||
|
case oasisSdk.ChatStatePaused:
|
||||||
|
case oasisSdk.ChatStateInactive:
|
||||||
|
case oasisSdk.ChatStateGone:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
|
||||||
|
fmt.Printf("Delivered %s to %s", id, from.String())
|
||||||
|
},
|
||||||
|
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
|
||||||
|
fmt.Printf("%s has seen %s", from.String(), id)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not create client - " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err = client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not connect - " + err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
a := app.New()
|
||||||
|
a.Settings().SetTheme(myTheme{})
|
||||||
|
w := a.NewWindow("pi")
|
||||||
|
w.Resize(fyne.NewSize(500, 500))
|
||||||
|
|
||||||
|
entry := widget.NewMultiLineEntry()
|
||||||
|
entry.SetPlaceHolder("Say something, you know you want to.")
|
||||||
|
|
||||||
|
sendbtn := widget.NewButton("Send", func() {
|
||||||
|
text := entry.Text
|
||||||
|
go func() {
|
||||||
|
msg := oasisSdk.XMPPChatMessage{ // TODO: Remove hack when oasisSdk adds message sending (hopefully xd)
|
||||||
|
Message: stanza.Message{
|
||||||
|
To: jid.MustParse("ringen@muc.isekai.rocks"), //FIXME
|
||||||
|
Type: stanza.GroupChatMessage, //FIXME
|
||||||
|
},
|
||||||
|
ChatMessageBody: oasisSdk.ChatMessageBody{
|
||||||
|
Body: &text,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := client.Session.Encode(client.Ctx, msg)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
entry.SetText("")
|
||||||
|
})
|
||||||
|
|
||||||
|
mit := fyne.NewMenuItem("About pi", func() {
|
||||||
|
dialog.ShowInformation("About pi", "the XMPP client from hell\n\npi is an experimental XMPP client\nwritten by Sunglocto in Go.", w)
|
||||||
|
})
|
||||||
|
|
||||||
|
mia := fyne.NewMenuItem("Configure message view", func() {
|
||||||
|
ch := widget.NewCheck("", func(b bool) {})
|
||||||
|
ch.Checked = scrollDownOnNewMessage
|
||||||
|
scrollView := widget.NewFormItem("Scroll to bottom on new message", ch)
|
||||||
|
items := []*widget.FormItem{
|
||||||
|
scrollView,
|
||||||
|
}
|
||||||
|
dialog.ShowForm("Configure message view", "Apply", "Cancel", items, func(b bool) {
|
||||||
|
if b {
|
||||||
|
scrollDownOnNewMessage = ch.Checked
|
||||||
|
}
|
||||||
|
}, w)
|
||||||
|
})
|
||||||
|
mib := fyne.NewMenuItem("Join a room", func() {
|
||||||
|
nick := widget.NewEntry()
|
||||||
|
room := widget.NewEntry()
|
||||||
|
items := []*widget.FormItem{
|
||||||
|
widget.NewFormItem("Nick", nick),
|
||||||
|
widget.NewFormItem("MUC address", room),
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.ShowForm("Join a MUC", "Join", "Cancel", items, func(b bool) {
|
||||||
|
if b {
|
||||||
|
fmt.Println("attempting to join MUC")
|
||||||
|
fmt.Println(nick)
|
||||||
|
fmt.Println(room)
|
||||||
|
go func() {
|
||||||
|
client.MucClient.Join(client.Ctx, jid.MustParse(room.Text), client.Session, nil)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}, w)
|
||||||
|
})
|
||||||
|
menu_help := fyne.NewMenu("π", mit)
|
||||||
|
menu_changeroom := fyne.NewMenu("β", mib)
|
||||||
|
menu_configureview := fyne.NewMenu("γ", mia)
|
||||||
|
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview)
|
||||||
|
w.SetMainMenu(ma)
|
||||||
|
tabs := container.NewAppTabs(container.NewTabItem("pi", widget.NewLabel("pi\n\nthe XMPP client from hell")), container.NewTabItem("chat", scroller))
|
||||||
|
w.SetContent(container.NewVSplit(tabs, container.NewHSplit(entry, sendbtn))) // TODO: Add send message functionality
|
||||||
|
w.ShowAndRun()
|
||||||
|
}
|
141
pi.svg
Normal file
141
pi.svg
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="200mm"
|
||||||
|
height="200mm"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="pi.svg"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.38395763"
|
||||||
|
inkscape:cx="65.111351"
|
||||||
|
inkscape:cy="388.06365"
|
||||||
|
inkscape:window-width="1876"
|
||||||
|
inkscape:window-height="1000"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g3" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g1"
|
||||||
|
transform="matrix(0.70699338,0,0,0.70699338,36.332006,51.956657)">
|
||||||
|
<g
|
||||||
|
id="g3">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:172.637px;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#210063;fill-opacity:0.697983;stroke:#7400ae;stroke-width:14.3864;stroke-opacity:1"
|
||||||
|
x="25.983902"
|
||||||
|
y="131.06415"
|
||||||
|
id="text1-1"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan1-5"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Serif;-inkscape-font-specification:Serif;fill:#210063;fill-opacity:0.697983;stroke:#7400ae;stroke-width:14.3864;stroke-opacity:1"
|
||||||
|
x="25.983902"
|
||||||
|
y="131.06415">π</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:172.637px;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#b118ff;fill-opacity:1;stroke:#b118ff;stroke-width:14.3864;stroke-opacity:1"
|
||||||
|
x="35.94326"
|
||||||
|
y="121.42939"
|
||||||
|
id="text1"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan1"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Serif;-inkscape-font-specification:Serif;fill:#b118ff;fill-opacity:1;stroke:#b118ff;stroke-width:14.3864;stroke-opacity:1"
|
||||||
|
x="35.94326"
|
||||||
|
y="121.42939">π</tspan></text>
|
||||||
|
<g
|
||||||
|
id="g4-6"
|
||||||
|
transform="translate(-7.9610819,-28.846801)">
|
||||||
|
<ellipse
|
||||||
|
style="fill:#530075;fill-opacity:1;stroke:none;stroke-width:0.848664;stroke-miterlimit:26.7;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path3-4"
|
||||||
|
cx="169.40752"
|
||||||
|
cy="45.499199"
|
||||||
|
rx="10.234183"
|
||||||
|
ry="10.32556" />
|
||||||
|
<rect
|
||||||
|
style="fill:#530075;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="rect3-3"
|
||||||
|
width="2.9291241"
|
||||||
|
height="22.571486"
|
||||||
|
x="165.08534"
|
||||||
|
y="-90.200768"
|
||||||
|
transform="rotate(33.369596)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#530075;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="rect3-9-3"
|
||||||
|
width="2.9291241"
|
||||||
|
height="22.571486"
|
||||||
|
x="86.228531"
|
||||||
|
y="-188.09276"
|
||||||
|
transform="rotate(75.044324)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#530075;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="rect3-9-8-3"
|
||||||
|
width="2.9291241"
|
||||||
|
height="22.571486"
|
||||||
|
x="-28.242762"
|
||||||
|
y="-208.65135"
|
||||||
|
transform="rotate(113.84012)" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g4"
|
||||||
|
transform="translate(-6.8227879,-31.189888)">
|
||||||
|
<ellipse
|
||||||
|
style="fill:#9600d2;fill-opacity:1;stroke:none;stroke-width:0.848664;stroke-miterlimit:26.7;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path3"
|
||||||
|
cx="169.40752"
|
||||||
|
cy="45.499199"
|
||||||
|
rx="10.234183"
|
||||||
|
ry="10.32556" />
|
||||||
|
<rect
|
||||||
|
style="fill:#9600d2;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="rect3"
|
||||||
|
width="2.9291241"
|
||||||
|
height="22.571486"
|
||||||
|
x="165.08534"
|
||||||
|
y="-90.200768"
|
||||||
|
transform="rotate(33.369596)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#9600d2;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="rect3-9"
|
||||||
|
width="2.9291241"
|
||||||
|
height="22.571486"
|
||||||
|
x="86.228531"
|
||||||
|
y="-188.09276"
|
||||||
|
transform="rotate(75.044324)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#9600d2;fill-opacity:1;stroke:none;stroke-width:1.77871;stroke-miterlimit:26.7;stroke-dasharray:0.533614, 0.533614;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="rect3-9-8"
|
||||||
|
width="2.9291241"
|
||||||
|
height="22.571486"
|
||||||
|
x="-28.242762"
|
||||||
|
y="-208.65135"
|
||||||
|
transform="rotate(113.84012)" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.8 KiB |
Reference in New Issue
Block a user