Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5568009a3e | ||
![]() |
4d9794a87a | ||
![]() |
e0afd88423 | ||
![]() |
93384bb99b | ||
5986461b59 | |||
5e4957a1e4 | |||
431736e967 | |||
02c4862178 | |||
ce4e5e40b3 | |||
da657875a1 | |||
eb1c585ecb | |||
d929a7d8dd | |||
dba2b73e56 | |||
e26cf0ffa7 | |||
2ba5ce4c82 | |||
213b812e28 | |||
21af7e2b8b | |||
0f195d05cc | |||
a44d35d1f8 | |||
0a43cbdf3f | |||
0e00a3a721 | |||
544de1df8b | |||
2ea0414710 | |||
1adcb41089 | |||
3d1c707fc7 | |||
88f02f2e11 | |||
46778bc646 | |||
ba57aefd4b | |||
85d63ff9b2 | |||
b501c30ca3 | |||
![]() |
91c2c7f35e | ||
697ff09936 | |||
![]() |
857c31bcba | ||
![]() |
07a92f02ef | ||
![]() |
995d698e7d | ||
![]() |
346a3c971c | ||
![]() |
253168e1b5 | ||
cf4d6e4ebe | |||
![]() |
f88d0606a6 | ||
e737ebd1ce | |||
b2bc620c35 | |||
![]() |
7a1a9d4636 |
1
.gitignore
vendored
@ -4,3 +4,4 @@
|
|||||||
/config.yml
|
/config.yml
|
||||||
/bRAC
|
/bRAC
|
||||||
/.direnv/
|
/.direnv/
|
||||||
|
/package/
|
||||||
|
532
Cargo.lock
generated
10
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bRAC"
|
name = "bRAC"
|
||||||
version = "0.1.6+2.0"
|
version = "0.1.7+2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@ -10,9 +10,8 @@ lazy_static = "1.5.0"
|
|||||||
serde_yml = "0.0.12"
|
serde_yml = "0.0.12"
|
||||||
homedir = "0.3.4"
|
homedir = "0.3.4"
|
||||||
native-tls = "0.2.14"
|
native-tls = "0.2.14"
|
||||||
clap = { version = "4.5.36", features = ["derive"] }
|
clap = { version = "4.5.36", features = ["derive", "cargo"] }
|
||||||
serde = { version = "1.0.219", features = ["serde_derive"] }
|
serde = { version = "1.0.219", features = ["serde_derive"] }
|
||||||
gtk4 = { version = "0.9.6", optional = true }
|
|
||||||
chrono = "0.4.40"
|
chrono = "0.4.40"
|
||||||
serde_default = "0.2.0"
|
serde_default = "0.2.0"
|
||||||
socks = "0.3.4"
|
socks = "0.3.4"
|
||||||
@ -21,14 +20,15 @@ notify-rust = { version = "4.11.7", optional = true }
|
|||||||
gdk-pixbuf = { version = "0.3.0", optional = true } # DO NOT UPDATE
|
gdk-pixbuf = { version = "0.3.0", optional = true } # DO NOT UPDATE
|
||||||
winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] }
|
winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] }
|
||||||
tungstenite = "0.27.0"
|
tungstenite = "0.27.0"
|
||||||
reqwest = { version = "0.12.20", features = ["blocking"] }
|
reqwest = { version = "0.12.20", features = ["blocking", "socks"] }
|
||||||
|
libadwaita = { version = "0.8.0", optional = true, features = ["v1_6"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
winresource = { version = "0.1.20", optional = true }
|
winresource = { version = "0.1.20", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["gtk"]
|
default = ["gtk"]
|
||||||
gtk = ["dep:gtk4"]
|
gtk = ["dep:libadwaita"]
|
||||||
libnotify = ["dep:libnotify", "dep:gdk-pixbuf"]
|
libnotify = ["dep:libnotify", "dep:gdk-pixbuf"]
|
||||||
notify-rust = ["dep:notify-rust"]
|
notify-rust = ["dep:notify-rust"]
|
||||||
winapi = ["dep:winapi", "dep:winresource"]
|
winapi = ["dep:winapi", "dep:winresource"]
|
||||||
|
57
Makefile
@ -1,42 +1,27 @@
|
|||||||
.PHONY: clean install uninstall build_linux build_windows build_all
|
.PHONY: clean install uninstall package
|
||||||
|
|
||||||
TARGETS = \
|
|
||||||
i686-unknown-linux-gnu \
|
|
||||||
i686-unknown-linux-musl \
|
|
||||||
x86_64-unknown-linux-none \
|
|
||||||
x86_64-unknown-linux-gnu \
|
|
||||||
x86_64-unknown-linux-musl \
|
|
||||||
aarch64-unknown-linux-gnu \
|
|
||||||
aarch64-unknown-linux-musl
|
|
||||||
|
|
||||||
install: target/release/bRAC
|
|
||||||
mkdir -p ~/.local
|
|
||||||
mkdir -p ~/.local/bin
|
|
||||||
mkdir -p ~/.local/share
|
|
||||||
cp $< ~/.local/bin/bRAC
|
|
||||||
chmod +x ~/.local/bin/bRAC
|
|
||||||
mkdir ~/.local/share/bRAC -p
|
|
||||||
cp misc/bRAC.png ~/.local/share/bRAC/icon.png
|
|
||||||
./misc/create-desktop.sh > ~/.local/share/applications/ru.themixray.bRAC.desktop
|
|
||||||
uninstall:
|
|
||||||
rm -rf ~/.config/bRAC ~/.local/share/bRAC
|
|
||||||
rm -f ~/.local/share/applications/ru.themixray.bRAC.desktop
|
|
||||||
target/release/bRAC:
|
target/release/bRAC:
|
||||||
cargo build -r
|
cargo build -r
|
||||||
|
|
||||||
|
install: target/release/bRAC
|
||||||
|
mkdir -p /usr/bin
|
||||||
|
mkdir -p /usr/share
|
||||||
|
cp $< /usr/bin/bRAC
|
||||||
|
chmod +x /usr/bin/bRAC
|
||||||
|
cp misc/bRAC.png /usr/share/pixmaps/ru.themixray.bRAC.png
|
||||||
|
chmod +x misc/create-desktop.sh
|
||||||
|
./misc/create-desktop.sh > /usr/share/applications/ru.themixray.bRAC.desktop
|
||||||
|
uninstall:
|
||||||
|
rm -f /usr/bin/bRAC
|
||||||
|
rm -f /usr/share/applications/ru.themixray.bRAC.desktop
|
||||||
|
rm -f /usr/share/pixmaps/ru.themixray.bRAC.png
|
||||||
|
|
||||||
build_all: build_linux build_windows
|
package:
|
||||||
|
./misc/build.sh
|
||||||
build_linux:
|
mkdir -p package
|
||||||
mkdir -p build
|
for i in $$( ls build/*.zip ); do \
|
||||||
mkdir -p build/linux
|
mv $$i package/bRAC-$$(basename $$i); \
|
||||||
for target in $(TARGETS); do \
|
|
||||||
cargo build -r --target $$target; \
|
|
||||||
cp target/$$target/bRAC build/linux/$$target-bRAC; \
|
|
||||||
done
|
done
|
||||||
|
|
||||||
build_windows:
|
|
||||||
echo "Windows build is in development!!!"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
cargo clean
|
rm -rf build package target
|
||||||
rm -rf build
|
|
||||||
|
30
README.md
@ -9,11 +9,13 @@ better RAC client
|
|||||||
|
|
||||||
## features
|
## features
|
||||||
|
|
||||||
- gtk4 modern GUI
|
- gtk4 (libadwaita) modern GUI
|
||||||
- RACv1.99.x and RACv2.0 compatible
|
- RACv1.99.x and RACv2.0 compatible
|
||||||
- WRAC compatible ([docs](docs/wrac.md))
|
- WRAC compatible ([docs](docs/wrac.md))
|
||||||
|
- avatars drawing ([docs](docs/avatars.md))
|
||||||
- chat commands (type /help)
|
- chat commands (type /help)
|
||||||
- no ip and date visible for anyone (almost)
|
- no ip and date visible for anyone (almost)
|
||||||
|
- libtesl environment support
|
||||||
- coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc.)
|
- coloring usernames by their clients (CRAB, clRAC, Mefidroniy, etc.)
|
||||||
- many command-line options (see --help)
|
- many command-line options (see --help)
|
||||||
- rich configuration (--config-path to get file path)
|
- rich configuration (--config-path to get file path)
|
||||||
@ -24,9 +26,11 @@ better RAC client
|
|||||||
|
|
||||||
## how to run
|
## how to run
|
||||||
|
|
||||||
### install (recommended)
|
### download binary
|
||||||
|
|
||||||
**for linux:**
|
go to [releases](https://github.com/MeexReay/bRAC/releases/latest) and download file you need. its simple.
|
||||||
|
|
||||||
|
### install (for linux)
|
||||||
|
|
||||||
1. Install requirements: `gtk4-dev gtk4 make rust git openssl-dev openssl`
|
1. Install requirements: `gtk4-dev gtk4 make rust git openssl-dev openssl`
|
||||||
|
|
||||||
@ -34,16 +38,6 @@ better RAC client
|
|||||||
|
|
||||||
3. Now you are able to start bRAC with desktop icon
|
3. Now you are able to start bRAC with desktop icon
|
||||||
|
|
||||||
**for windows:**
|
|
||||||
|
|
||||||
NO SOLUTION
|
|
||||||
|
|
||||||
Read [compiling docs](docs/compiling.md) to build it manually.
|
|
||||||
|
|
||||||
### download binary
|
|
||||||
|
|
||||||
go to [releases](https://github.com/MeexReay/bRAC/releases/latest) and download file you need. its simple.
|
|
||||||
|
|
||||||
### build from source
|
### build from source
|
||||||
|
|
||||||
1. Make sure [Rust](https://www.rust-lang.org/tools/install) is installed
|
1. Make sure [Rust](https://www.rust-lang.org/tools/install) is installed
|
||||||
@ -56,13 +50,12 @@ cd bRAC
|
|||||||
|
|
||||||
3. Run with Cargo
|
3. Run with Cargo
|
||||||
```bash
|
```bash
|
||||||
cargo build -r # build release (target/release/bRAC)
|
cargo build -r # build release (target/release/bRAC)
|
||||||
cargo run -r # build and run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Read more about that on the [compiling docs](docs/compiling.md).
|
Read more about that on the [compiling docs](docs/compiling.md).
|
||||||
|
|
||||||
### nix package
|
### nixos installation
|
||||||
|
|
||||||
If you have Nix package manager installed, you can use:
|
If you have Nix package manager installed, you can use:
|
||||||
|
|
||||||
@ -71,6 +64,9 @@ nix build github:MeexReay/bRAC # build release (result/bin/bRAC)
|
|||||||
nix run github:MeexReay/bRAC # build and run
|
nix run github:MeexReay/bRAC # build and run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To install, you need to add it to your [system flake](https://nixos.wiki/wiki/flakes#Using_nix_flakes_with_NixOS) \
|
||||||
|
[Example of such installation from my dotfiles](https://git.meex.lol/MeexReay/dotfiles-nix/commit/ea4c8f18a54da8ae2d260bc714873a877f8e79e0)
|
||||||
|
|
||||||
## chat commands
|
## chat commands
|
||||||
|
|
||||||
commands are any messages that start with a slash `/` \
|
commands are any messages that start with a slash `/` \
|
||||||
@ -96,7 +92,7 @@ messages starting with a slash are sent to chat only if the `--disable-commands`
|
|||||||
|
|
||||||
## see also
|
## see also
|
||||||
|
|
||||||
- [Racinfo - webpage with info about RAC](https://racinfo.kostyazero.com/)
|
- [Racinfo - webpage with info about RAC](https://racinfo.meex.lol/)
|
||||||
- [RAC-Hub - all about RAC protocol](https://meexreay.github.io/RAC-Hub/)
|
- [RAC-Hub - all about RAC protocol](https://meexreay.github.io/RAC-Hub/)
|
||||||
- [RAC protocol (v2.0)](https://gitea.bedohswe.eu.org/pixtaded/crab#rac-protocol)
|
- [RAC protocol (v2.0)](https://gitea.bedohswe.eu.org/pixtaded/crab#rac-protocol)
|
||||||
- [CRAB - client & server for RAC](https://gitea.bedohswe.eu.org/pixtaded/crab)
|
- [CRAB - client & server for RAC](https://gitea.bedohswe.eu.org/pixtaded/crab)
|
||||||
|
@ -59,27 +59,44 @@ Libnotify sucks in many situations, but it always work
|
|||||||
- Make a desktop file:
|
- Make a desktop file:
|
||||||
|
|
||||||
Enter the repository folder and run: `./misc/create-desktop.sh` \
|
Enter the repository folder and run: `./misc/create-desktop.sh` \
|
||||||
You'll get a desktop file contents, just edit paths here and write it to a new file in the `~/.local/share/applications` or `/usr/share/applications`\
|
You'll get a desktop file contents, just edit paths here and write it to a new file `ru.themixray.bRAC.desktop` in the `~/.local/share/applications` or `/usr/share/applications`\
|
||||||
All of these, with adding icons and other, makes this command: `make install` (using `gnumake` package) \
|
All of these, with adding icons and other, makes this command: `make install` (using `gnumake` package) \
|
||||||
But make sure, that you have `.local/bin` in the `PATH` variable, otherwise it won't work. \
|
But make sure, that you have `.local/bin` in the `PATH` variable, otherwise it won't work. \
|
||||||
Now, if you'll run with the desktop file, GNotifications will work perfectly.
|
Now, when you run that desktop file, GNotifications will work correctly.
|
||||||
|
|
||||||
# Cross-compiling
|
### Error: have you installed the static version of the ? library
|
||||||
|
|
||||||
## From Linux to Windows
|
Use this rustflags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RUSTFLAGS="-C target-feature=-crt-static" cargo build -r
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dark theme dont work
|
||||||
|
|
||||||
|
Edit `~/.config/gtk-4.0/settings.ini`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Settings]
|
||||||
|
gtk-application-prefer-dark-theme=1
|
||||||
|
```
|
||||||
|
|
||||||
|
# Cross-compiling (from Linux)
|
||||||
|
|
||||||
|
Build for all supported systems:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./misc/build.sh
|
./misc/build.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## From NixOS to Windows
|
The result will be in `build/` directory.
|
||||||
|
|
||||||
```bash
|
Supported systems:
|
||||||
nix-shell -p rustup gcc cargo-cross zip unzip curl
|
|
||||||
rustup toolchain install stable
|
|
||||||
./misc/build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## From Windows to Linux
|
- `windows-x86_64`
|
||||||
|
- `linux-x86_64`
|
||||||
|
|
||||||
That's your problem
|
To clean up the build, run `./misc/build.sh clean`
|
||||||
|
|
||||||
|
To build for only one system, run `./misc/build.sh <system>`,
|
||||||
|
example: `./misc/build.sh linux-x86_64`
|
||||||
|
14
flake.nix
@ -7,10 +7,10 @@
|
|||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
|
outputs = { nixpkgs, rust-overlay, flake-utils, ... }:
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
let
|
let
|
||||||
devDeps = with pkgs; [ pkg-config openssl gtk4 pango libnotify ];
|
devDeps = with pkgs; [ pkg-config openssl gtk4 pango libnotify libadwaita ];
|
||||||
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||||
overlays = [ (import rust-overlay) ];
|
overlays = [ (import rust-overlay) ];
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
@ -25,18 +25,18 @@
|
|||||||
nativeBuildInputs = devDeps ++ [ rustc ];
|
nativeBuildInputs = devDeps ++ [ rustc ];
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
devShells.nightly = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)));
|
devShells.default = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)));
|
||||||
devShells.default = (mkDevShell pkgs.rust-bin.stable.latest.default);
|
devShells.stable = (mkDevShell pkgs.rust-bin.stable.latest.default);
|
||||||
|
|
||||||
packages.default = (pkgs.makeRustPlatform {
|
packages.default = (pkgs.makeRustPlatform {
|
||||||
cargo = pkgs.rust-bin.stable.latest.minimal;
|
cargo = pkgs.rust-bin.nightly.latest.minimal;
|
||||||
rustc = pkgs.rust-bin.stable.latest.minimal;
|
rustc = pkgs.rust-bin.nightly.latest.minimal;
|
||||||
}).buildRustPackage {
|
}).buildRustPackage {
|
||||||
inherit (cargoToml.package) name version;
|
inherit (cargoToml.package) name version;
|
||||||
src = ./.;
|
src = ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
buildInputs = devDeps;
|
buildInputs = devDeps;
|
||||||
nativeBuildInputs = devDeps ++ [ pkgs.rustc ];
|
nativeBuildInputs = devDeps;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,35 +1,44 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# echo "Run this script only from repository root!"
|
SUPPORTED_SYSTEMS=('')
|
||||||
# echo "This script depends on:"
|
|
||||||
# echo " - fact that you are on linux x86_64!"
|
|
||||||
# echo " - zip, unzip, curl. install it with your distro's package manager"
|
|
||||||
# echo " - cross crate. to install it, run this: cargo install cross --git https://github.com/cross-rs/cross"
|
|
||||||
# echo " - docker, so you should run something like this on your distro: sudo systemctl start docker"
|
|
||||||
# read -p "Press enter if you really want to do rm -rf build/"
|
|
||||||
|
|
||||||
build_linux() {
|
build() {
|
||||||
mkdir build/linux-x86_64
|
local build_dir=build/$1
|
||||||
mkdir build/linux-x86_64/misc
|
|
||||||
|
|
||||||
|
rm -rf $build_dir
|
||||||
|
mkdir $build_dir
|
||||||
|
mkdir $build_dir/misc
|
||||||
|
|
||||||
|
$1 # build
|
||||||
|
|
||||||
|
# copy readme, license and make a zip
|
||||||
|
cp README.md $build_dir
|
||||||
|
cp LICENSE $build_dir
|
||||||
|
zip -r $build_dir.zip $build_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
SUPPORTED_SYSTEMS+=('linux-x86_64')
|
||||||
|
linux-x86_64() {
|
||||||
# add gnotification version and install script
|
# add gnotification version and install script
|
||||||
cargo build -r
|
cargo build -r
|
||||||
cp target/release/bRAC build/linux-x86_64/misc/bRAC-gnotif
|
cp target/release/bRAC $build_dir/misc/bRAC-gnotif
|
||||||
cp misc/user-install.sh build/linux-x86_64/install.sh
|
cp misc/bRAC.png $build_dir/misc
|
||||||
cp misc/bRAC.png build/linux-x86_64/misc
|
|
||||||
cp misc/create-desktop.sh build/linux-x86_64/misc
|
cp misc/user-install.sh $build_dir/install.sh
|
||||||
|
cp misc/user-uninstall.sh $build_dir/uninstall.sh
|
||||||
|
cp misc/create-desktop.sh $build_dir/misc
|
||||||
|
|
||||||
|
chmod +x $build_dir/install.sh
|
||||||
|
chmod +x $build_dir/uninstall.sh
|
||||||
|
chmod +x $build_dir/misc/create-desktop.sh
|
||||||
|
|
||||||
# add libnotify version as the alternative
|
# add libnotify version as the alternative
|
||||||
cargo build -r -F libnotify
|
cargo build -r -F libnotify
|
||||||
cp target/release/bRAC build/linux-x86_64
|
cp target/release/bRAC build/linux-x86_64
|
||||||
|
|
||||||
# copy readme, license and make a zip
|
|
||||||
cp README.md build/linux-x86_64
|
|
||||||
cp LICENSE build/linux-x86_64
|
|
||||||
zip -r build/bRAC-linux-x86_64.zip build/linux-x86_64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
build_windows() {
|
SUPPORTED_SYSTEMS+=('windows-x86_64')
|
||||||
|
windows-x86_64() {
|
||||||
docker run -ti -v `pwd`:/mnt mglolenstine/gtk4-cross:rust-gtk-nightly /bin/bash -c "
|
docker run -ti -v `pwd`:/mnt mglolenstine/gtk4-cross:rust-gtk-nightly /bin/bash -c "
|
||||||
source \"\$HOME/.cargo/env\";
|
source \"\$HOME/.cargo/env\";
|
||||||
rustup update nightly; # update nightly toolchain
|
rustup update nightly; # update nightly toolchain
|
||||||
@ -37,36 +46,22 @@ build_windows() {
|
|||||||
sed -i -e 's/cargo build/cargo +nightly build -F notify-rust,winapi/g' /bin/build; # add features + nightly
|
sed -i -e 's/cargo build/cargo +nightly build -F notify-rust,winapi/g' /bin/build; # add features + nightly
|
||||||
build; # build it, creates package dir
|
build; # build it, creates package dir
|
||||||
package; # package it (adds some libs)
|
package; # package it (adds some libs)
|
||||||
mv package build/windows-x86_64;
|
mv package $build_dir;
|
||||||
chmod -R 777 build/windows-x86_64;
|
chmod -R 777 $build_dir;
|
||||||
chmod -R 777 target"
|
chmod -R 777 target"
|
||||||
|
}
|
||||||
# copy readme, license and make a zip
|
|
||||||
cp README.md build/windows-x86_64
|
|
||||||
cp LICENSE build/windows-x86_64
|
|
||||||
zip -r build/bRAC-windows-x86_64.zip build/windows-x86_64
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
|
|
||||||
if [ $# -eq 0 ]; then
|
if [ $# -eq 0 ]; then
|
||||||
if [ ! -d build/windows-x86_64 ]; then
|
for system in "${SUPPORTED_SYSTEMS[@]}"; do
|
||||||
build_windows
|
if [ ! -d build/$system ]; then build $system; fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
if [ $1 = "clean" ]; then
|
||||||
|
rm -rf build
|
||||||
|
elif [[ ${SUPPORTED_SYSTEMS[@]} =~ " $1" ]]; then
|
||||||
|
build $1;
|
||||||
fi
|
fi
|
||||||
if [ ! -d build/linux-x86_64 ]; then
|
|
||||||
build_linux
|
|
||||||
fi
|
|
||||||
exit
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $1 = "clean" ]; then
|
|
||||||
rm -rf build
|
|
||||||
elif [ $1 = "windows" ]; then
|
|
||||||
rm -rf build/windows-x86_64
|
|
||||||
build_windows
|
|
||||||
elif [ $1 = "linux" ]; then
|
|
||||||
rm -rf build/linux-x86_64
|
|
||||||
build_linux
|
|
||||||
else
|
|
||||||
echo "possible arguments: clean windows linux. none for auto"
|
|
||||||
fi
|
|
||||||
|
4
misc/create-desktop.sh
Normal file → Executable file
@ -7,8 +7,8 @@ echo "Name=bRAC"
|
|||||||
echo "Version=$version"
|
echo "Version=$version"
|
||||||
echo "Type=Application"
|
echo "Type=Application"
|
||||||
echo "Comment=better RAC client"
|
echo "Comment=better RAC client"
|
||||||
echo "Icon=$HOME/.local/share/bRAC/icon.png"
|
echo "Icon=ru.themixray.bRAC"
|
||||||
echo "Exec=$HOME/.local/bin/bRAC"
|
echo "Exec=/usr/bin/bRAC"
|
||||||
echo "Categories=Network;"
|
echo "Categories=Network;"
|
||||||
echo "StartupNotify=true"
|
echo "StartupNotify=true"
|
||||||
echo "Terminal=false"
|
echo "Terminal=false"
|
||||||
|
12
misc/install.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
mkdir -p /usr/bin
|
||||||
|
mkdir -p /usr/share
|
||||||
|
mkdir -p /usr/share/pixmaps
|
||||||
|
|
||||||
|
cp misc/bRAC-gnotif /usr/bin/bRAC
|
||||||
|
chmod +x /usr/bin/bRAC
|
||||||
|
|
||||||
|
cp misc/bRAC.png /usr/share/pixmaps/ru.themixray.bRAC.png
|
||||||
|
chmod +x misc/create-desktop.sh
|
||||||
|
./misc/create-desktop.sh > /usr/share/applications/ru.themixray.bRAC.desktop
|
@ -1,30 +0,0 @@
|
|||||||
@echo off
|
|
||||||
net session >nul 2>&1 || (
|
|
||||||
echo This script requires administrator privileges.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
set "DEST=C:\Program Files\bRAC"
|
|
||||||
mkdir "%DEST%" 2>nul
|
|
||||||
xcopy "." "%DEST%\" /E /I /H /Y >nul
|
|
||||||
|
|
||||||
for /d %%u in ("C:\Users\*") do (
|
|
||||||
if exist "%%u\AppData\Roaming\Microsoft\Windows\Desktop" (
|
|
||||||
call :s "%%u\AppData\Roaming\Microsoft\Windows\Desktop\bRAC.lnk" "%DEST%\bRAC.exe"
|
|
||||||
) else if exist "%%u\Desktop" (
|
|
||||||
call :s "%%u\Desktop\bRAC.lnk" "%DEST%\bRAC.exe"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
exit /b
|
|
||||||
|
|
||||||
:s
|
|
||||||
set "v=%TEMP%\_s.vbs"
|
|
||||||
> "%v%" echo Set o=CreateObject("WScript.Shell")
|
|
||||||
>>"%v%" echo Set l=o.CreateShortcut("%~1")
|
|
||||||
>>"%v%" echo l.TargetPath="%~2"
|
|
||||||
>>"%v%" echo l.WorkingDirectory="%~dp2"
|
|
||||||
>>"%v%" echo l.Save
|
|
||||||
wscript "%v%" >nul
|
|
||||||
del "%v%" >nul
|
|
||||||
exit /b
|
|
@ -1,13 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "this script is deprecated, fix it yourself if you wanna to"; exit
|
|
||||||
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
|
||||||
echo "This script must be run as root"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cp bRAC /bin/bRAC
|
|
||||||
chmod +x /bin/bRAC
|
|
||||||
cp ru.themixray.bRAC.png /usr/share/pixmaps
|
|
||||||
cp ru.themixray.bRAC.desktop /usr/share/applications
|
|
@ -1,35 +0,0 @@
|
|||||||
@echo off
|
|
||||||
net session >nul 2>&1 || (
|
|
||||||
echo This script requires administrator privileges.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
set "TARGET=C:\Program Files\bRAC\bRAC.exe"
|
|
||||||
|
|
||||||
for /d %%u in ("C:\Users\*") do (
|
|
||||||
call :d "%%u\AppData\Roaming\Microsoft\Windows\Desktop"
|
|
||||||
call :d "%%u\Desktop"
|
|
||||||
)
|
|
||||||
|
|
||||||
cd /d "%TEMP%"
|
|
||||||
rmdir /s /q "C:\Program Files\bRAC"
|
|
||||||
exit /b
|
|
||||||
|
|
||||||
:d
|
|
||||||
if not exist "%~1" exit /b
|
|
||||||
for %%f in ("%~1\*.lnk") do (
|
|
||||||
call :c "%%~f"
|
|
||||||
)
|
|
||||||
exit /b
|
|
||||||
|
|
||||||
:c
|
|
||||||
set "v=%TEMP%\_c.vbs"
|
|
||||||
> "%v%" echo Set o=CreateObject("WScript.Shell")
|
|
||||||
>>"%v%" echo Set l=o.CreateShortcut("%~1")
|
|
||||||
>>"%v%" echo WScript.Echo l.TargetPath
|
|
||||||
for /f "usebackq delims=" %%t in (`wscript //nologo "%v%"`) do (
|
|
||||||
if /I "%%t"=="%TARGET%" del /f /q "%~1"
|
|
||||||
)
|
|
||||||
del "%v%" >nul
|
|
||||||
exit /b
|
|
@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "this script is deprecated, fix it yourself if you wanna to"; exit
|
|
||||||
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
|
||||||
echo "This script must be run as root"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
getent passwd | while IFS=: read -r name password uid gid gecos home shell; do
|
|
||||||
rm -rf $home/.config/bRAC;
|
|
||||||
done
|
|
||||||
|
|
||||||
rm -f /bin/bRAC
|
|
||||||
rm -f /usr/share/pixmaps/ru.themixray.bRAC.png
|
|
||||||
rm -f /usr/share/applications/ru.themixray.bRAC.desktop
|
|
5
misc/uninstall.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
rm -rf /usr/bin/bRAC \
|
||||||
|
/usr/share/pixmaps/ru.themixray.bRAC.png \
|
||||||
|
/usr/share/applications/ru.themixray.bRAC.desktop
|
@ -1,12 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
mkdir -p ~/.local
|
|
||||||
mkdir -p ~/.local/bin
|
|
||||||
mkdir -p ~/.local/share
|
|
||||||
mkdir -p ~/.local/share/bRAC
|
|
||||||
|
|
||||||
cp misc/bRAC-gnotif ~/.local/bin/bRAC
|
|
||||||
chmod +x ~/.local/bin/bRAC
|
|
||||||
|
|
||||||
cp misc/bRAC.png ~/.local/share/bRAC/icon.png
|
|
||||||
./misc/create-desktop.sh > ~/.local/share/applications/ru.themixray.bRAC.desktop
|
|
@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
rm -rf ~/.config/bRAC ~/.local/share/bRAC ~/.local/share/applications/ru.themixray.bRAC.desktop
|
|
@ -3,6 +3,8 @@ use serde_default::DefaultFromSerde;
|
|||||||
use serde_yml;
|
use serde_yml;
|
||||||
use std::{error::Error, fs, path::PathBuf};
|
use std::{error::Error, fs, path::PathBuf};
|
||||||
|
|
||||||
|
use super::SERVER_LIST;
|
||||||
|
|
||||||
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
|
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
@ -11,6 +13,9 @@ fn default_true() -> bool {
|
|||||||
pub fn default_max_messages() -> usize {
|
pub fn default_max_messages() -> usize {
|
||||||
200
|
200
|
||||||
}
|
}
|
||||||
|
pub fn default_max_avatar_size() -> u64 {
|
||||||
|
5248000 // 5MB
|
||||||
|
}
|
||||||
pub fn default_update_time() -> usize {
|
pub fn default_update_time() -> usize {
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
@ -27,6 +32,10 @@ pub fn default_message_format() -> String {
|
|||||||
MESSAGE_FORMAT.to_string()
|
MESSAGE_FORMAT.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_servers() -> Vec<String> {
|
||||||
|
SERVER_LIST.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, DefaultFromSerde, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, DefaultFromSerde, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_host")]
|
#[serde(default = "default_host")]
|
||||||
@ -41,6 +50,8 @@ pub struct Config {
|
|||||||
pub oof_update_time: usize,
|
pub oof_update_time: usize,
|
||||||
#[serde(default = "default_max_messages")]
|
#[serde(default = "default_max_messages")]
|
||||||
pub max_messages: usize,
|
pub max_messages: usize,
|
||||||
|
#[serde(default = "default_max_avatar_size")]
|
||||||
|
pub max_avatar_size: u64,
|
||||||
#[serde(default = "default_konata_size")]
|
#[serde(default = "default_konata_size")]
|
||||||
pub konata_size: usize,
|
pub konata_size: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -65,6 +76,8 @@ pub struct Config {
|
|||||||
pub debug_logs: bool,
|
pub debug_logs: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
|
#[serde(default = "default_servers")]
|
||||||
|
pub servers: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@ -149,6 +162,8 @@ pub struct Args {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub update_time: Option<usize>,
|
pub update_time: Option<usize>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
pub max_avatar_size: Option<u64>,
|
||||||
|
#[arg(long)]
|
||||||
pub oof_update_time: Option<usize>,
|
pub oof_update_time: Option<usize>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub max_messages: Option<usize>,
|
pub max_messages: Option<usize>,
|
||||||
@ -176,6 +191,7 @@ pub struct Args {
|
|||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub debug_logs: bool,
|
pub debug_logs: bool,
|
||||||
|
// TODO: add servers
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
@ -204,6 +220,9 @@ impl Args {
|
|||||||
if let Some(v) = self.konata_size {
|
if let Some(v) = self.konata_size {
|
||||||
config.konata_size = v
|
config.konata_size = v
|
||||||
}
|
}
|
||||||
|
if let Some(v) = self.max_avatar_size {
|
||||||
|
config.max_avatar_size = v
|
||||||
|
}
|
||||||
if let Some(v) = self.hide_my_ip {
|
if let Some(v) = self.hide_my_ip {
|
||||||
config.hide_my_ip = v
|
config.hide_my_ip = v
|
||||||
}
|
}
|
||||||
|
1337
src/chat/gui.rs
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
BIN
src/chat/gui/images/servers.png
Normal file
After Width: | Height: | Size: 26 KiB |
802
src/chat/gui/mod.rs
Normal file
@ -0,0 +1,802 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::hash::{DefaultHasher, Hasher};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::AtomicU64;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::sync::{atomic::Ordering, mpsc::channel, Arc, RwLock};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use clap::crate_version;
|
||||||
|
|
||||||
|
use adw::gdk::Display;
|
||||||
|
use adw::gio::{ActionEntry, ApplicationFlags, Menu};
|
||||||
|
use adw::glib::clone;
|
||||||
|
use adw::glib::{self, source::timeout_add_local_once, timeout_add_once};
|
||||||
|
use adw::prelude::*;
|
||||||
|
use adw::{Application, ApplicationWindow};
|
||||||
|
use libadwaita::gdk::Texture;
|
||||||
|
use libadwaita::gtk::gdk_pixbuf::InterpType;
|
||||||
|
use libadwaita::gtk::{Button, Entry, Label, Picture};
|
||||||
|
use libadwaita::{self as adw, Avatar, Breakpoint, BreakpointCondition, Dialog, OverlaySplitView};
|
||||||
|
|
||||||
|
use adw::gtk;
|
||||||
|
use gtk::gdk_pixbuf::{Pixbuf, PixbufLoader};
|
||||||
|
use gtk::{Box as GtkBox, CssProvider, Orientation, ScrolledWindow, Settings};
|
||||||
|
|
||||||
|
use crate::chat::grab_avatar;
|
||||||
|
|
||||||
|
use super::config::get_config_path;
|
||||||
|
use super::{
|
||||||
|
config::{save_config, Config},
|
||||||
|
ctx::Context,
|
||||||
|
print_message, recv_tick, sanitize_message,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod page;
|
||||||
|
mod preferences;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
|
use page::*;
|
||||||
|
use preferences::*;
|
||||||
|
|
||||||
|
pub fn try_save_config(path: PathBuf, config: &Config) {
|
||||||
|
match save_config(path, config) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("save config error: {e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UiModel {
|
||||||
|
is_dark_theme: bool,
|
||||||
|
chat_box: GtkBox,
|
||||||
|
chat_scrolled: ScrolledWindow,
|
||||||
|
app: Application,
|
||||||
|
window: ApplicationWindow,
|
||||||
|
#[cfg(feature = "libnotify")]
|
||||||
|
notifications: Arc<RwLock<Vec<libnotify::Notification>>>,
|
||||||
|
#[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))]
|
||||||
|
notifications: Arc<RwLock<Vec<String>>>,
|
||||||
|
avatars: Arc<Mutex<HashMap<u64, Vec<Avatar>>>>,
|
||||||
|
latest_sign: Arc<AtomicU64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local!(
|
||||||
|
static GLOBAL: RefCell<Option<UiModel>> = RefCell::new(None);
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn clear_chat_messages(ctx: Arc<Context>, messages: Vec<String>) {
|
||||||
|
let _ = ctx
|
||||||
|
.sender
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.unwrap()
|
||||||
|
.send((messages, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_chat_messages(ctx: Arc<Context>, messages: Vec<String>) {
|
||||||
|
println!("add chat messages: {}", messages.len());
|
||||||
|
let _ = ctx
|
||||||
|
.sender
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.unwrap()
|
||||||
|
.send((messages, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_pixbuf(data: &[u8]) -> Result<Pixbuf, Box<dyn Error>> {
|
||||||
|
let loader = PixbufLoader::new();
|
||||||
|
loader.write(data)?;
|
||||||
|
loader.close()?;
|
||||||
|
Ok(loader.pixbuf().ok_or("laod pixbuf error")?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_window_title(ctx: Arc<Context>) {
|
||||||
|
GLOBAL.with(|global| {
|
||||||
|
if let Some(ui) = &*global.borrow() {
|
||||||
|
ui.window.set_title(Some(&format!(
|
||||||
|
"bRAC - Connected to {} as {}",
|
||||||
|
ctx.config(|o| o.host.clone()),
|
||||||
|
&ctx.name()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_menu(ctx: Arc<Context>, app: &Application) -> Menu {
|
||||||
|
let menu = Menu::new();
|
||||||
|
|
||||||
|
menu.append(Some("Settings"), Some("app.settings"));
|
||||||
|
menu.append(Some("About"), Some("app.about"));
|
||||||
|
menu.append(Some("Close"), Some("app.close"));
|
||||||
|
|
||||||
|
app.add_action_entries([
|
||||||
|
ActionEntry::builder("settings")
|
||||||
|
.activate(clone!(
|
||||||
|
#[weak]
|
||||||
|
ctx,
|
||||||
|
move |a: &Application, _, _| {
|
||||||
|
open_settings(ctx, a);
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.build(),
|
||||||
|
ActionEntry::builder("close")
|
||||||
|
.activate(move |a: &Application, _, _| {
|
||||||
|
a.quit();
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
ActionEntry::builder("about")
|
||||||
|
.activate(clone!(
|
||||||
|
#[weak]
|
||||||
|
app,
|
||||||
|
move |_, _, _| {
|
||||||
|
let dialog = adw::AboutDialog::builder()
|
||||||
|
.developer_name("MeexReay")
|
||||||
|
.license(glib::markup_escape_text(include_str!("../../../LICENSE")))
|
||||||
|
.comments("better RAC client")
|
||||||
|
.website("https://github.com/MeexReay/bRAC")
|
||||||
|
.application_name("bRAC")
|
||||||
|
.application_icon("ru.themixray.bRAC")
|
||||||
|
.version(crate_version!())
|
||||||
|
.build();
|
||||||
|
dialog.present(app.active_window().as_ref());
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.build(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
menu
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_sidebar_button(
|
||||||
|
ctx: Arc<Context>,
|
||||||
|
split_view: &OverlaySplitView,
|
||||||
|
server: String,
|
||||||
|
servers_list: &GtkBox,
|
||||||
|
) -> GtkBox {
|
||||||
|
let hbox = GtkBox::new(Orientation::Horizontal, 5);
|
||||||
|
|
||||||
|
let button = Button::builder().label(&server).hexpand(true).build();
|
||||||
|
|
||||||
|
button.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
split_view,
|
||||||
|
#[weak]
|
||||||
|
ctx,
|
||||||
|
#[strong]
|
||||||
|
server,
|
||||||
|
move |_| {
|
||||||
|
let mut config = ctx.config.read().unwrap().clone();
|
||||||
|
config.host = server.clone();
|
||||||
|
ctx.set_config(&config);
|
||||||
|
try_save_config(get_config_path(), &config);
|
||||||
|
update_window_title(ctx.clone());
|
||||||
|
if split_view.is_collapsed() {
|
||||||
|
split_view.set_show_sidebar(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
hbox.append(&button);
|
||||||
|
|
||||||
|
let delete_button = Button::from_icon_name("user-trash-symbolic");
|
||||||
|
|
||||||
|
delete_button.set_css_classes(&["destructive-action"]);
|
||||||
|
|
||||||
|
delete_button.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
ctx,
|
||||||
|
#[weak]
|
||||||
|
hbox,
|
||||||
|
#[weak]
|
||||||
|
servers_list,
|
||||||
|
#[strong]
|
||||||
|
server,
|
||||||
|
move |_| {
|
||||||
|
servers_list.remove(&hbox);
|
||||||
|
let mut config = ctx.config.read().unwrap().clone();
|
||||||
|
let index = config.servers.iter().position(|x| *x == server).unwrap();
|
||||||
|
config.servers.remove(index);
|
||||||
|
ctx.set_config(&config);
|
||||||
|
try_save_config(get_config_path(), &config);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
hbox.append(&delete_button);
|
||||||
|
|
||||||
|
hbox
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_sidebar(ctx: Arc<Context>, app: &Application, split_view: &OverlaySplitView) -> GtkBox {
|
||||||
|
let sidebar = GtkBox::new(Orientation::Vertical, 15);
|
||||||
|
|
||||||
|
sidebar.set_margin_start(5);
|
||||||
|
sidebar.set_margin_end(5);
|
||||||
|
|
||||||
|
sidebar.append(
|
||||||
|
&Picture::builder()
|
||||||
|
.paintable(&Texture::for_pixbuf(
|
||||||
|
&load_pixbuf(include_bytes!("images/servers.png")).unwrap(),
|
||||||
|
))
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let servers_list = GtkBox::new(Orientation::Vertical, 5);
|
||||||
|
|
||||||
|
for server in ctx.config(|o| o.servers.clone()) {
|
||||||
|
servers_list.append(&build_sidebar_button(
|
||||||
|
ctx.clone(),
|
||||||
|
&split_view,
|
||||||
|
server,
|
||||||
|
&servers_list,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebar.append(&servers_list);
|
||||||
|
|
||||||
|
let add_server = Button::builder()
|
||||||
|
.label("Add Server")
|
||||||
|
// .start_icon_name("list-add-symbolic")
|
||||||
|
.css_classes(["suggested-action"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
add_server.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
app,
|
||||||
|
#[weak]
|
||||||
|
servers_list,
|
||||||
|
#[weak]
|
||||||
|
ctx,
|
||||||
|
#[weak]
|
||||||
|
split_view,
|
||||||
|
move |_| {
|
||||||
|
let dialog = Dialog::new();
|
||||||
|
|
||||||
|
let vbox = GtkBox::new(Orientation::Vertical, 10);
|
||||||
|
|
||||||
|
vbox.set_margin_bottom(20);
|
||||||
|
vbox.set_margin_top(20);
|
||||||
|
vbox.set_margin_end(20);
|
||||||
|
vbox.set_margin_start(20);
|
||||||
|
|
||||||
|
vbox.append(
|
||||||
|
&Label::builder()
|
||||||
|
.label("Add server")
|
||||||
|
.css_classes(["title-2"])
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let entry = Entry::builder()
|
||||||
|
.placeholder_text("Server host")
|
||||||
|
.hexpand(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
vbox.append(&entry);
|
||||||
|
|
||||||
|
let hbox = GtkBox::new(Orientation::Horizontal, 5);
|
||||||
|
|
||||||
|
let confirm = Button::builder()
|
||||||
|
.label("Confirm")
|
||||||
|
.hexpand(true)
|
||||||
|
.css_classes(["suggested-action"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
confirm.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
dialog,
|
||||||
|
#[weak]
|
||||||
|
servers_list,
|
||||||
|
#[weak]
|
||||||
|
ctx,
|
||||||
|
#[weak]
|
||||||
|
split_view,
|
||||||
|
#[weak]
|
||||||
|
entry,
|
||||||
|
move |_| {
|
||||||
|
let server: String = entry.text().into();
|
||||||
|
|
||||||
|
let mut config = ctx.config.read().unwrap().clone();
|
||||||
|
config.servers.push(server.clone());
|
||||||
|
ctx.set_config(&config);
|
||||||
|
try_save_config(get_config_path(), &config);
|
||||||
|
|
||||||
|
servers_list.append(&build_sidebar_button(
|
||||||
|
ctx.clone(),
|
||||||
|
&split_view,
|
||||||
|
server,
|
||||||
|
&servers_list,
|
||||||
|
));
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
hbox.append(&confirm);
|
||||||
|
|
||||||
|
let cancel = Button::builder().label("Cancel").hexpand(true).build();
|
||||||
|
|
||||||
|
cancel.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
dialog,
|
||||||
|
move |_| {
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
hbox.append(&cancel);
|
||||||
|
|
||||||
|
vbox.append(&hbox);
|
||||||
|
|
||||||
|
dialog.set_child(Some(&vbox));
|
||||||
|
|
||||||
|
dialog.present(app.active_window().as_ref());
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
sidebar.append(&add_server);
|
||||||
|
|
||||||
|
sidebar
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
|
||||||
|
let is_dark_theme = if let Some(settings) = Settings::default() {
|
||||||
|
settings.is_gtk_application_prefer_dark_theme()
|
||||||
|
|| settings
|
||||||
|
.gtk_theme_name()
|
||||||
|
.map(|o| o.to_lowercase().contains("dark"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let is_dark_theme = true;
|
||||||
|
|
||||||
|
let main_box = GtkBox::new(Orientation::Vertical, 0);
|
||||||
|
|
||||||
|
let (header, page, chat_box, chat_scrolled) = build_page(ctx.clone(), app);
|
||||||
|
|
||||||
|
let split_view = OverlaySplitView::builder()
|
||||||
|
.content(&page)
|
||||||
|
.enable_hide_gesture(true)
|
||||||
|
.enable_show_gesture(true)
|
||||||
|
.collapsed(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let sidebar = build_sidebar(ctx.clone(), &app, &split_view);
|
||||||
|
|
||||||
|
split_view.set_sidebar(Some(&sidebar));
|
||||||
|
|
||||||
|
main_box.append(&split_view);
|
||||||
|
|
||||||
|
let toggle_button = Button::from_icon_name("go-previous-symbolic");
|
||||||
|
|
||||||
|
toggle_button.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
split_view,
|
||||||
|
move |_| {
|
||||||
|
split_view.set_show_sidebar(!split_view.shows_sidebar());
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
header.pack_start(&toggle_button);
|
||||||
|
|
||||||
|
let window = ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.title(&format!(
|
||||||
|
"bRAC - Connected to {} as {}",
|
||||||
|
ctx.config(|o| o.host.clone()),
|
||||||
|
&ctx.name()
|
||||||
|
))
|
||||||
|
.default_width(500)
|
||||||
|
.default_height(500)
|
||||||
|
.resizable(true)
|
||||||
|
.decorated(true)
|
||||||
|
.content(&main_box)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let breakpoint = Breakpoint::new(BreakpointCondition::new_length(
|
||||||
|
libadwaita::BreakpointConditionLengthType::MinWidth,
|
||||||
|
700.0,
|
||||||
|
libadwaita::LengthUnit::Px,
|
||||||
|
));
|
||||||
|
|
||||||
|
breakpoint.add_setter(&split_view, "collapsed", Some(&false.into()));
|
||||||
|
breakpoint.add_setter(&toggle_button, "visible", Some(&false.into()));
|
||||||
|
|
||||||
|
window.add_breakpoint(breakpoint);
|
||||||
|
|
||||||
|
window.present();
|
||||||
|
|
||||||
|
UiModel {
|
||||||
|
is_dark_theme,
|
||||||
|
chat_scrolled,
|
||||||
|
chat_box,
|
||||||
|
app: app.clone(),
|
||||||
|
window: window.clone(),
|
||||||
|
#[cfg(feature = "libnotify")]
|
||||||
|
notifications: Arc::new(RwLock::new(Vec::<libnotify::Notification>::new())),
|
||||||
|
#[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))]
|
||||||
|
notifications: Arc::new(RwLock::new(Vec::<String>::new())),
|
||||||
|
avatars: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
latest_sign: Arc::new(AtomicU64::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
|
||||||
|
let (sender, receiver) = channel();
|
||||||
|
|
||||||
|
*ctx.sender.write().unwrap() = Some(Arc::new(sender));
|
||||||
|
|
||||||
|
run_recv_loop(ctx.clone());
|
||||||
|
|
||||||
|
ui.window.connect_notify(Some("is-active"), {
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
|
||||||
|
move |a, _| {
|
||||||
|
let is_focused = a.is_active();
|
||||||
|
|
||||||
|
ctx.is_focused.store(is_focused, Ordering::SeqCst);
|
||||||
|
|
||||||
|
if is_focused {
|
||||||
|
thread::spawn({
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
move || {
|
||||||
|
make_recv_tick(ctx.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(not(feature = "notify-rust"))]
|
||||||
|
GLOBAL.with(|global| {
|
||||||
|
if let Some(ui) = &*global.borrow() {
|
||||||
|
#[cfg(feature = "libnotify")]
|
||||||
|
for i in ui.notifications.read().unwrap().clone() {
|
||||||
|
i.close().expect("libnotify close error");
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "libnotify"))]
|
||||||
|
for i in ui.notifications.read().unwrap().clone() {
|
||||||
|
ui.app.withdraw_notification(&i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
GLOBAL.with(|global| {
|
||||||
|
*global.borrow_mut() = Some(ui);
|
||||||
|
});
|
||||||
|
|
||||||
|
thread::spawn({
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
move || {
|
||||||
|
while let Ok((messages, clear)) = receiver.recv() {
|
||||||
|
println!("got chat messages: {}", messages.len());
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let messages = Arc::new(messages);
|
||||||
|
|
||||||
|
timeout_add_once(Duration::ZERO, {
|
||||||
|
let messages = messages.clone();
|
||||||
|
|
||||||
|
move || {
|
||||||
|
GLOBAL.with(|global| {
|
||||||
|
if let Some(ui) = &*global.borrow() {
|
||||||
|
if clear {
|
||||||
|
while let Some(row) = ui.chat_box.last_child() {
|
||||||
|
ui.chat_box.remove(&row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for message in messages.iter() {
|
||||||
|
on_add_message(ctx.clone(), &ui, message.to_string(), !clear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ctx.config(|o| !o.new_ui_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
for message in messages.iter() {
|
||||||
|
let Some(avatar_url) = grab_avatar(message) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let avatar_id = get_avatar_id(&avatar_url);
|
||||||
|
|
||||||
|
let Some(avatar) = load_avatar(
|
||||||
|
&avatar_url,
|
||||||
|
ctx.config(|o| o.proxy.clone()),
|
||||||
|
ctx.config(|o| o.max_avatar_size as usize),
|
||||||
|
) else {
|
||||||
|
println!("cant load avatar: {avatar_url} request error");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(pixbuf) = load_pixbuf(&avatar) else {
|
||||||
|
println!("cant load avatar: {avatar_url} pixbuf error");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(pixbuf) =
|
||||||
|
pixbuf.scale_simple(32, 32, InterpType::Bilinear)
|
||||||
|
else {
|
||||||
|
println!("cant load avatar: {avatar_url} scale image error");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let texture = Texture::for_pixbuf(&pixbuf);
|
||||||
|
|
||||||
|
timeout_add_once(Duration::ZERO, {
|
||||||
|
move || {
|
||||||
|
GLOBAL.with(|global| {
|
||||||
|
if let Some(ui) = &*global.borrow() {
|
||||||
|
if let Some(pics) =
|
||||||
|
ui.avatars.lock().unwrap().remove(&avatar_id)
|
||||||
|
{
|
||||||
|
for pic in pics {
|
||||||
|
pic.set_custom_image(Some(&texture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_css(is_dark_theme: bool) {
|
||||||
|
let provider = CssProvider::new();
|
||||||
|
provider.load_from_data(&format!(
|
||||||
|
"{}\n{}",
|
||||||
|
if is_dark_theme {
|
||||||
|
include_str!("styles/dark.css")
|
||||||
|
} else {
|
||||||
|
include_str!("styles/light.css")
|
||||||
|
},
|
||||||
|
include_str!("styles/style.css")
|
||||||
|
));
|
||||||
|
|
||||||
|
gtk::style_context_add_provider_for_display(
|
||||||
|
&Display::default().expect("Could not connect to a display."),
|
||||||
|
&provider,
|
||||||
|
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "notify-rust")]
|
||||||
|
fn send_notification(_: Arc<Context>, _: &UiModel, title: &str, message: &str) {
|
||||||
|
use notify_rust::{Notification, Timeout};
|
||||||
|
|
||||||
|
Notification::new()
|
||||||
|
.summary(title)
|
||||||
|
.body(message)
|
||||||
|
.auto_icon()
|
||||||
|
.appname("bRAC")
|
||||||
|
.timeout(Timeout::Default) // this however is
|
||||||
|
.show()
|
||||||
|
.expect("notify-rust send error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libnotify")]
|
||||||
|
fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str) {
|
||||||
|
use libnotify::Notification;
|
||||||
|
|
||||||
|
let notification = Notification::new(title, message, None);
|
||||||
|
notification.set_app_name("bRAC");
|
||||||
|
let pixbuf_loader = gdk_pixbuf::PixbufLoader::new();
|
||||||
|
pixbuf_loader
|
||||||
|
.loader_write(include_bytes!("images/icon.png"))
|
||||||
|
.unwrap();
|
||||||
|
pixbuf_loader.close().unwrap();
|
||||||
|
notification.set_image_from_pixbuf(&pixbuf_loader.get_pixbuf().unwrap());
|
||||||
|
notification.show().expect("libnotify send error");
|
||||||
|
|
||||||
|
ui.notifications.write().unwrap().push(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))]
|
||||||
|
fn send_notification(_: Arc<Context>, ui: &UiModel, title: &str, message: &str) {
|
||||||
|
use std::{
|
||||||
|
hash::{DefaultHasher, Hasher},
|
||||||
|
time::UNIX_EPOCH,
|
||||||
|
};
|
||||||
|
|
||||||
|
use gtk::gio::Notification;
|
||||||
|
|
||||||
|
let mut hash = DefaultHasher::new();
|
||||||
|
hash.write(title.as_bytes());
|
||||||
|
hash.write(message.as_bytes());
|
||||||
|
|
||||||
|
let id = format!(
|
||||||
|
"bRAC-{}-{}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
hash.finish()
|
||||||
|
);
|
||||||
|
|
||||||
|
let notif = Notification::new(title);
|
||||||
|
notif.set_body(Some(&message));
|
||||||
|
ui.app.send_notification(Some(&id), ¬if);
|
||||||
|
|
||||||
|
ui.notifications.write().unwrap().push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_avatar_id(url: &str) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
hasher.write(url.as_bytes());
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_avatar(url: &str, proxy: Option<String>, response_limit: usize) -> Option<Vec<u8>> {
|
||||||
|
let client = if let Some(proxy) = proxy {
|
||||||
|
let proxy = if proxy.starts_with("socks5://") {
|
||||||
|
proxy
|
||||||
|
} else {
|
||||||
|
format!("socks5://{proxy}")
|
||||||
|
};
|
||||||
|
|
||||||
|
reqwest::blocking::Client::builder()
|
||||||
|
.proxy(reqwest::Proxy::all(&proxy).ok()?)
|
||||||
|
.build()
|
||||||
|
.ok()?
|
||||||
|
} else {
|
||||||
|
reqwest::blocking::Client::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
client.get(url).send().ok().and_then(|mut resp| {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
let mut length = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if length >= response_limit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut buf = vec![0; (response_limit - length).min(1024)];
|
||||||
|
let now_len = resp.read(&mut buf).ok()?;
|
||||||
|
if now_len == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf.truncate(now_len);
|
||||||
|
length += now_len;
|
||||||
|
data.append(&mut buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates sign that expires in 0-20 minutes
|
||||||
|
fn get_message_sign(name: &str, date: &str) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
hasher.write(name.as_bytes());
|
||||||
|
hasher.write(date[..date.len() - 2].as_bytes());
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns message sign
|
||||||
|
fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool) {
|
||||||
|
let notify = notify && ctx.config(|c| c.notifications_enabled);
|
||||||
|
|
||||||
|
let formatting_enabled = ctx.config(|c| c.formatting_enabled);
|
||||||
|
|
||||||
|
let Some(sanitized) = (if formatting_enabled {
|
||||||
|
sanitize_message(message.clone())
|
||||||
|
} else {
|
||||||
|
Some(message.clone())
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if sanitized.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.config(|o| o.new_ui_enabled) {
|
||||||
|
ui.chat_box.append(&get_new_message_box(
|
||||||
|
ctx.clone(),
|
||||||
|
ui,
|
||||||
|
message,
|
||||||
|
notify,
|
||||||
|
formatting_enabled,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ui.chat_box.append(&get_message_box(
|
||||||
|
ctx.clone(),
|
||||||
|
ui,
|
||||||
|
message,
|
||||||
|
notify,
|
||||||
|
formatting_enabled,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
timeout_add_local_once(Duration::from_millis(1000), move || {
|
||||||
|
GLOBAL.with(|global| {
|
||||||
|
if let Some(ui) = &*global.borrow() {
|
||||||
|
let o = &ui.chat_scrolled;
|
||||||
|
o.vadjustment()
|
||||||
|
.set_value(o.vadjustment().upper() - o.vadjustment().page_size());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_recv_tick(ctx: Arc<Context>) {
|
||||||
|
if let Err(e) = recv_tick(ctx.clone()) {
|
||||||
|
if ctx.config(|o| o.debug_logs) {
|
||||||
|
let _ = print_message(
|
||||||
|
ctx.clone(),
|
||||||
|
format!("Print messages error: {}", e.to_string()).to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_recv_loop(ctx: Arc<Context>) {
|
||||||
|
thread::spawn(move || loop {
|
||||||
|
make_recv_tick(ctx.clone());
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(
|
||||||
|
if ctx.is_focused.load(Ordering::SeqCst) {
|
||||||
|
ctx.config(|o| o.update_time) as u64
|
||||||
|
} else {
|
||||||
|
ctx.config(|o| o.oof_update_time) as u64
|
||||||
|
},
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_main_loop(ctx: Arc<Context>) {
|
||||||
|
#[cfg(feature = "libnotify")]
|
||||||
|
{
|
||||||
|
libnotify::init("ru.themixray.bRAC").expect("libnotify init error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use std::env;
|
||||||
|
env::set_var("GTK_THEME", "Adwaita:dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
let application = Application::builder()
|
||||||
|
.application_id("ru.themixray.bRAC")
|
||||||
|
.flags(ApplicationFlags::FLAGS_NONE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
application.connect_activate({
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
|
||||||
|
move |app| {
|
||||||
|
let ui = build_ui(ctx.clone(), app);
|
||||||
|
load_css(ui.is_dark_theme);
|
||||||
|
setup(app, ctx.clone(), ui);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
application.connect_startup({
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
|
||||||
|
move |app| {
|
||||||
|
build_menu(ctx.clone(), app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
application.run_with_args::<&str>(&[]);
|
||||||
|
|
||||||
|
#[cfg(feature = "libnotify")]
|
||||||
|
{
|
||||||
|
libnotify::uninit();
|
||||||
|
}
|
||||||
|
}
|
618
src/chat/gui/page.rs
Normal file
@ -0,0 +1,618 @@
|
|||||||
|
use std::sync::{atomic::Ordering, Arc};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
|
use adw::gdk::{Cursor, Display};
|
||||||
|
use adw::gio::MemoryInputStream;
|
||||||
|
use adw::glib::clone;
|
||||||
|
use adw::glib::{self, source::timeout_add_local_once, timeout_add_local, ControlFlow};
|
||||||
|
use adw::prelude::*;
|
||||||
|
use adw::Application;
|
||||||
|
use libadwaita::gdk::{BUTTON_PRIMARY, BUTTON_SECONDARY};
|
||||||
|
use libadwaita::gtk::{GestureLongPress, MenuButton, Popover};
|
||||||
|
use libadwaita::{self as adw, Avatar, HeaderBar, ToolbarView};
|
||||||
|
|
||||||
|
use adw::gtk;
|
||||||
|
use gtk::gdk_pixbuf::PixbufAnimation;
|
||||||
|
use gtk::pango::WrapMode;
|
||||||
|
use gtk::{
|
||||||
|
Align, Box as GtkBox, Button, Calendar, Entry, Fixed, GestureClick, Justification, Label,
|
||||||
|
ListBox, Orientation, Overlay, Picture, ScrolledWindow,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::chat::{
|
||||||
|
config::get_config_path, ctx::Context, on_send_message, parse_message, SERVER_LIST,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::widgets::CustomLayout;
|
||||||
|
use super::{
|
||||||
|
add_chat_messages, build_menu, get_avatar_id, get_message_sign, load_pixbuf, send_notification,
|
||||||
|
try_save_config, update_window_title, UiModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn get_message_box(
|
||||||
|
ctx: Arc<Context>,
|
||||||
|
ui: &UiModel,
|
||||||
|
message: String,
|
||||||
|
notify: bool,
|
||||||
|
formatting_enabled: bool,
|
||||||
|
) -> GtkBox {
|
||||||
|
// TODO: softcode these colors
|
||||||
|
|
||||||
|
let (ip_color, date_color, text_color) = if ui.is_dark_theme {
|
||||||
|
("#494949", "#929292", "#FFFFFF")
|
||||||
|
} else {
|
||||||
|
("#585858", "#292929", "#000000")
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut label = String::new();
|
||||||
|
|
||||||
|
if let (true, Some((date, ip, content, nick, _))) =
|
||||||
|
(formatting_enabled, parse_message(message.clone()))
|
||||||
|
{
|
||||||
|
if let Some(ip) = ip {
|
||||||
|
if ctx.config(|o| o.show_other_ip) {
|
||||||
|
label.push_str(&format!(
|
||||||
|
"<span color=\"{ip_color}\">{}</span> ",
|
||||||
|
glib::markup_escape_text(&ip)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label.push_str(&format!(
|
||||||
|
"<span color=\"{date_color}\">[{}]</span> ",
|
||||||
|
glib::markup_escape_text(&date)
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some((name, color)) = nick {
|
||||||
|
label.push_str(&format!(
|
||||||
|
"<span font_weight=\"bold\" color=\"{}\"><{}></span> ",
|
||||||
|
color.to_uppercase(),
|
||||||
|
glib::markup_escape_text(&name)
|
||||||
|
));
|
||||||
|
|
||||||
|
if notify && !ui.window.is_active() {
|
||||||
|
if ctx.config(|o| o.chunked_enabled) {
|
||||||
|
send_notification(
|
||||||
|
ctx.clone(),
|
||||||
|
ui,
|
||||||
|
&format!("{}'s Message", &name),
|
||||||
|
&glib::markup_escape_text(&content),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if notify && !ui.window.is_active() {
|
||||||
|
if ctx.config(|o| o.chunked_enabled) {
|
||||||
|
send_notification(ctx.clone(), ui, "System Message", &content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label.push_str(&format!(
|
||||||
|
"<span color=\"{text_color}\">{}</span>",
|
||||||
|
glib::markup_escape_text(&content)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
label.push_str(&format!(
|
||||||
|
"<span color=\"{text_color}\">{}</span>",
|
||||||
|
glib::markup_escape_text(&message)
|
||||||
|
));
|
||||||
|
|
||||||
|
if notify && !ui.window.is_active() {
|
||||||
|
if ctx.config(|o| o.chunked_enabled) {
|
||||||
|
send_notification(ctx.clone(), ui, "Chat Message", &message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hbox = GtkBox::new(Orientation::Horizontal, 2);
|
||||||
|
|
||||||
|
hbox.append(
|
||||||
|
&Label::builder()
|
||||||
|
.label(&label)
|
||||||
|
.halign(Align::Start)
|
||||||
|
.valign(Align::Start)
|
||||||
|
.selectable(true)
|
||||||
|
.wrap(true)
|
||||||
|
.wrap_mode(WrapMode::WordChar)
|
||||||
|
.use_markup(true)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
hbox.set_hexpand(true);
|
||||||
|
|
||||||
|
hbox
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_avatar_popup(avatar: String, avatar_picture: &Avatar) {
|
||||||
|
let display = Display::default().unwrap();
|
||||||
|
let clipboard = display.clipboard();
|
||||||
|
|
||||||
|
let popover = Popover::new();
|
||||||
|
|
||||||
|
let button = Button::with_label("Copy Link");
|
||||||
|
button.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
clipboard,
|
||||||
|
#[weak]
|
||||||
|
popover,
|
||||||
|
#[strong]
|
||||||
|
avatar,
|
||||||
|
move |_| {
|
||||||
|
clipboard.set_text(avatar.as_str());
|
||||||
|
popover.popdown();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let vbox = GtkBox::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(6)
|
||||||
|
.build();
|
||||||
|
vbox.append(&button);
|
||||||
|
|
||||||
|
popover.set_child(Some(&vbox));
|
||||||
|
popover.set_parent(avatar_picture);
|
||||||
|
popover.popup();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_new_message_box(
|
||||||
|
ctx: Arc<Context>,
|
||||||
|
ui: &UiModel,
|
||||||
|
message: String,
|
||||||
|
notify: bool,
|
||||||
|
formatting_enabled: bool,
|
||||||
|
) -> Overlay {
|
||||||
|
// TODO: softcode these colors
|
||||||
|
|
||||||
|
let (ip_color, date_color, text_color) = if ui.is_dark_theme {
|
||||||
|
("#494949", "#929292", "#FFFFFF")
|
||||||
|
} else {
|
||||||
|
("#585858", "#292929", "#000000")
|
||||||
|
};
|
||||||
|
|
||||||
|
let latest_sign = ui.latest_sign.load(Ordering::SeqCst);
|
||||||
|
|
||||||
|
let (date, ip, content, name, color, avatar, avatar_id) =
|
||||||
|
if let (true, Some((date, ip, content, nick, avatar))) =
|
||||||
|
(formatting_enabled, parse_message(message.clone()))
|
||||||
|
{
|
||||||
|
(
|
||||||
|
date,
|
||||||
|
ip,
|
||||||
|
content,
|
||||||
|
nick.as_ref()
|
||||||
|
.map(|o| o.0.to_string())
|
||||||
|
.unwrap_or("System".to_string()),
|
||||||
|
nick.as_ref()
|
||||||
|
.map(|o| o.1.to_string())
|
||||||
|
.unwrap_or("#DDDDDD".to_string()),
|
||||||
|
avatar.clone(),
|
||||||
|
avatar.map(|o| get_avatar_id(&o)).unwrap_or_default(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Local::now().format("%d.%m.%Y %H:%M").to_string(),
|
||||||
|
None,
|
||||||
|
message,
|
||||||
|
"System".to_string(),
|
||||||
|
"#DDDDDD".to_string(),
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if notify && !ui.window.is_active() {
|
||||||
|
if ctx.config(|o| o.chunked_enabled) {
|
||||||
|
send_notification(
|
||||||
|
ctx.clone(),
|
||||||
|
ui,
|
||||||
|
&if name == *"System" {
|
||||||
|
"System Message".to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}'s Message", name)
|
||||||
|
},
|
||||||
|
&glib::markup_escape_text(&content),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sign = get_message_sign(&name, &date);
|
||||||
|
|
||||||
|
let squashed = latest_sign == sign;
|
||||||
|
|
||||||
|
ui.latest_sign.store(sign, Ordering::SeqCst);
|
||||||
|
|
||||||
|
let overlay = Overlay::new();
|
||||||
|
|
||||||
|
if !squashed {
|
||||||
|
let fixed = Fixed::new();
|
||||||
|
fixed.set_can_target(false);
|
||||||
|
|
||||||
|
let avatar_picture = Avatar::builder()
|
||||||
|
.text(&name)
|
||||||
|
.show_initials(true)
|
||||||
|
.size(32)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
avatar_picture.set_vexpand(false);
|
||||||
|
avatar_picture.set_hexpand(false);
|
||||||
|
avatar_picture.set_valign(Align::Start);
|
||||||
|
avatar_picture.set_halign(Align::Start);
|
||||||
|
|
||||||
|
if let Some(avatar) = avatar {
|
||||||
|
let long_gesture = GestureLongPress::builder().button(BUTTON_PRIMARY).build();
|
||||||
|
|
||||||
|
long_gesture.connect_pressed(clone!(
|
||||||
|
#[weak]
|
||||||
|
avatar_picture,
|
||||||
|
#[strong]
|
||||||
|
avatar,
|
||||||
|
move |_, x, y| {
|
||||||
|
if x < 32.0 && y > 4.0 && y < 32.0 {
|
||||||
|
open_avatar_popup(avatar.clone(), &avatar_picture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
overlay.add_controller(long_gesture);
|
||||||
|
|
||||||
|
let short_gesture = GestureClick::builder().button(BUTTON_SECONDARY).build();
|
||||||
|
|
||||||
|
short_gesture.connect_released(clone!(
|
||||||
|
#[weak]
|
||||||
|
avatar_picture,
|
||||||
|
#[strong]
|
||||||
|
avatar,
|
||||||
|
move |_, _, x, y| {
|
||||||
|
if x < 32.0 && y > 4.0 && y < 32.0 {
|
||||||
|
open_avatar_popup(avatar.clone(), &avatar_picture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
overlay.add_controller(short_gesture);
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatar_id != 0 {
|
||||||
|
let mut lock = ui.avatars.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(pics) = lock.get_mut(&avatar_id) {
|
||||||
|
pics.push(avatar_picture.clone());
|
||||||
|
} else {
|
||||||
|
lock.insert(avatar_id, vec![avatar_picture.clone()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed.put(&avatar_picture, 0.0, 4.0);
|
||||||
|
|
||||||
|
overlay.add_overlay(&fixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let vbox = GtkBox::new(Orientation::Vertical, 2);
|
||||||
|
|
||||||
|
if !squashed {
|
||||||
|
vbox.append(&Label::builder()
|
||||||
|
.label(format!(
|
||||||
|
"<span color=\"{color}\">{}</span> <span color=\"{date_color}\">{}</span> <span color=\"{ip_color}\">{}</span>",
|
||||||
|
glib::markup_escape_text(&name),
|
||||||
|
glib::markup_escape_text(&date),
|
||||||
|
glib::markup_escape_text(&ip.unwrap_or_default()),
|
||||||
|
))
|
||||||
|
.halign(Align::Start)
|
||||||
|
.valign(Align::Start)
|
||||||
|
.selectable(true)
|
||||||
|
.wrap(true)
|
||||||
|
.wrap_mode(WrapMode::WordChar)
|
||||||
|
.use_markup(true)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
vbox.append(
|
||||||
|
&Label::builder()
|
||||||
|
.label(format!(
|
||||||
|
"<span color=\"{text_color}\">{}</span>",
|
||||||
|
glib::markup_escape_text(&content)
|
||||||
|
))
|
||||||
|
.halign(Align::Start)
|
||||||
|
.hexpand(true)
|
||||||
|
.selectable(true)
|
||||||
|
.wrap(true)
|
||||||
|
.wrap_mode(WrapMode::WordChar)
|
||||||
|
.use_markup(true)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
vbox.set_margin_start(37);
|
||||||
|
vbox.set_hexpand(true);
|
||||||
|
|
||||||
|
overlay.set_child(Some(&vbox));
|
||||||
|
|
||||||
|
if !squashed {
|
||||||
|
overlay.set_margin_top(7);
|
||||||
|
} else {
|
||||||
|
overlay.set_margin_top(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay
|
||||||
|
}
|
||||||
|
|
||||||
|
/// header, page_box, chat_box, chat_scrolled
|
||||||
|
pub fn build_page(
|
||||||
|
ctx: Arc<Context>,
|
||||||
|
app: &Application,
|
||||||
|
) -> (HeaderBar, GtkBox, GtkBox, ScrolledWindow) {
|
||||||
|
let page_box = GtkBox::new(Orientation::Vertical, 5);
|
||||||
|
page_box.set_css_classes(&["page-box"]);
|
||||||
|
|
||||||
|
let toolbar = ToolbarView::new();
|
||||||
|
|
||||||
|
let header = HeaderBar::new();
|
||||||
|
|
||||||
|
header.pack_end(
|
||||||
|
&MenuButton::builder()
|
||||||
|
.icon_name("open-menu-symbolic")
|
||||||
|
.menu_model(&build_menu(ctx.clone(), &app))
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
toolbar.set_content(Some(&header));
|
||||||
|
|
||||||
|
page_box.append(&toolbar);
|
||||||
|
|
||||||
|
page_box.append(&build_widget_box(ctx.clone(), app));
|
||||||
|
|
||||||
|
let chat_box = GtkBox::new(Orientation::Vertical, 2);
|
||||||
|
chat_box.set_css_classes(&["chat-box"]);
|
||||||
|
|
||||||
|
let chat_scrolled = ScrolledWindow::builder()
|
||||||
|
.child(&chat_box)
|
||||||
|
.vexpand(true)
|
||||||
|
.hexpand(true)
|
||||||
|
.margin_bottom(5)
|
||||||
|
.margin_end(5)
|
||||||
|
.margin_start(5)
|
||||||
|
.propagate_natural_height(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let layout = CustomLayout::default();
|
||||||
|
|
||||||
|
layout.connect_local("size-changed", false, {
|
||||||
|
let chat_scrolled = chat_scrolled.downgrade();
|
||||||
|
move |_| {
|
||||||
|
if let Some(chat_scrolled) = chat_scrolled.upgrade() {
|
||||||
|
let value =
|
||||||
|
chat_scrolled.vadjustment().upper() - chat_scrolled.vadjustment().page_size();
|
||||||
|
chat_scrolled.vadjustment().set_value(value);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page_box.set_layout_manager(Some(layout));
|
||||||
|
|
||||||
|
timeout_add_local_once(
|
||||||
|
Duration::ZERO,
|
||||||
|
clone!(
|
||||||
|
#[weak]
|
||||||
|
chat_scrolled,
|
||||||
|
move || {
|
||||||
|
let value =
|
||||||
|
chat_scrolled.vadjustment().upper() - chat_scrolled.vadjustment().page_size();
|
||||||
|
chat_scrolled.vadjustment().set_value(value);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
page_box.append(&chat_scrolled);
|
||||||
|
|
||||||
|
let send_box = GtkBox::new(Orientation::Horizontal, 5);
|
||||||
|
|
||||||
|
send_box.set_margin_bottom(5);
|
||||||
|
send_box.set_margin_end(5);
|
||||||
|
send_box.set_margin_start(5);
|
||||||
|
|
||||||
|
let text_entry = Entry::builder()
|
||||||
|
.placeholder_text("Message")
|
||||||
|
.css_classes(["send-button"])
|
||||||
|
.hexpand(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
send_box.append(&text_entry);
|
||||||
|
|
||||||
|
let send_btn = Button::builder()
|
||||||
|
.label("Send")
|
||||||
|
.css_classes(["send-text", "suggested-action"])
|
||||||
|
.cursor(&Cursor::from_name("pointer", None).unwrap())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
send_btn.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
text_entry,
|
||||||
|
#[weak]
|
||||||
|
ctx,
|
||||||
|
move |_| {
|
||||||
|
let text = text_entry.text().clone();
|
||||||
|
|
||||||
|
if text.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
text_entry.set_text("");
|
||||||
|
|
||||||
|
thread::spawn({
|
||||||
|
move || {
|
||||||
|
if let Err(e) = on_send_message(ctx.clone(), &text) {
|
||||||
|
if ctx.config(|o| o.debug_logs) {
|
||||||
|
let msg = format!("Send message error: {}", e.to_string()).to_string();
|
||||||
|
add_chat_messages(ctx.clone(), vec![msg]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
text_entry.connect_activate(clone!(
|
||||||
|
#[weak]
|
||||||
|
text_entry,
|
||||||
|
#[weak]
|
||||||
|
ctx,
|
||||||
|
move |_| {
|
||||||
|
let text = text_entry.text().clone();
|
||||||
|
|
||||||
|
if text.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
text_entry.set_text("");
|
||||||
|
|
||||||
|
thread::spawn({
|
||||||
|
move || {
|
||||||
|
if let Err(e) = on_send_message(ctx.clone(), &text) {
|
||||||
|
if ctx.config(|o| o.debug_logs) {
|
||||||
|
let msg = format!("Send message error: {}", e.to_string()).to_string();
|
||||||
|
add_chat_messages(ctx.clone(), vec![msg]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
send_box.append(&send_btn);
|
||||||
|
|
||||||
|
page_box.append(&send_box);
|
||||||
|
|
||||||
|
(header, page_box, chat_box, chat_scrolled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_widget_box(ctx: Arc<Context>, _app: &Application) -> Overlay {
|
||||||
|
let widget_box_overlay = Overlay::new();
|
||||||
|
|
||||||
|
let widget_box = GtkBox::new(Orientation::Horizontal, 5);
|
||||||
|
widget_box.set_css_classes(&["widget-box"]);
|
||||||
|
|
||||||
|
let remove_gui_shit = ctx.config(|c| c.remove_gui_shit);
|
||||||
|
|
||||||
|
if !remove_gui_shit {
|
||||||
|
widget_box.append(
|
||||||
|
&Calendar::builder()
|
||||||
|
.css_classes(["calendar"])
|
||||||
|
.show_heading(false)
|
||||||
|
.can_target(false)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let server_list_vbox = GtkBox::new(Orientation::Vertical, 5);
|
||||||
|
|
||||||
|
let server_list = ListBox::new();
|
||||||
|
|
||||||
|
for url in SERVER_LIST.iter() {
|
||||||
|
let url = url.to_string();
|
||||||
|
|
||||||
|
let label = Label::builder().label(&url).halign(Align::Start).build();
|
||||||
|
|
||||||
|
let click = GestureClick::new();
|
||||||
|
|
||||||
|
click.connect_pressed(clone!(
|
||||||
|
#[weak]
|
||||||
|
ctx,
|
||||||
|
move |_, _, _, _| {
|
||||||
|
let mut config = ctx.config.read().unwrap().clone();
|
||||||
|
config.host = url.clone();
|
||||||
|
ctx.set_config(&config);
|
||||||
|
try_save_config(get_config_path(), &config);
|
||||||
|
update_window_title(ctx.clone());
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
label.add_controller(click);
|
||||||
|
|
||||||
|
server_list.append(&label);
|
||||||
|
}
|
||||||
|
|
||||||
|
server_list_vbox.append(&Label::builder().label("Server List:").build());
|
||||||
|
|
||||||
|
server_list_vbox.append(&server_list);
|
||||||
|
|
||||||
|
widget_box.append(&server_list_vbox);
|
||||||
|
|
||||||
|
if !remove_gui_shit {
|
||||||
|
let fixed = Fixed::new();
|
||||||
|
fixed.set_can_target(false);
|
||||||
|
|
||||||
|
let konata_size = ctx.config(|c| c.konata_size) as i32;
|
||||||
|
|
||||||
|
let konata =
|
||||||
|
Picture::for_pixbuf(&load_pixbuf(include_bytes!("images/konata.png")).unwrap());
|
||||||
|
konata.set_size_request(174 * konata_size / 100, 127 * konata_size / 100);
|
||||||
|
|
||||||
|
fixed.put(
|
||||||
|
&konata,
|
||||||
|
(499 - 174 * konata_size / 100) as f64,
|
||||||
|
(131 - 127 * konata_size / 100) as f64,
|
||||||
|
);
|
||||||
|
|
||||||
|
let logo_gif = include_bytes!("images/logo.gif");
|
||||||
|
|
||||||
|
let logo = Picture::for_pixbuf(&load_pixbuf(logo_gif).unwrap());
|
||||||
|
logo.set_size_request(152 * konata_size / 100, 64 * konata_size / 100);
|
||||||
|
|
||||||
|
let logo_anim = PixbufAnimation::from_stream(
|
||||||
|
&MemoryInputStream::from_bytes(&glib::Bytes::from(logo_gif)),
|
||||||
|
None::<&adw::gtk::gio::Cancellable>,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.iter(Some(SystemTime::now()));
|
||||||
|
|
||||||
|
timeout_add_local(Duration::from_millis(30), {
|
||||||
|
let logo = logo.clone();
|
||||||
|
let logo_anim = logo_anim.clone();
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
|
||||||
|
move || {
|
||||||
|
if ctx.is_focused.load(Ordering::SeqCst) {
|
||||||
|
logo.set_pixbuf(Some(&logo_anim.pixbuf()));
|
||||||
|
logo_anim.advance(SystemTime::now());
|
||||||
|
}
|
||||||
|
ControlFlow::Continue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 262, 4
|
||||||
|
fixed.put(
|
||||||
|
&logo,
|
||||||
|
(436 - 174 * konata_size / 100) as f64,
|
||||||
|
(131 - 127 * konata_size / 100) as f64,
|
||||||
|
);
|
||||||
|
|
||||||
|
let time = Label::builder()
|
||||||
|
.label(&Local::now().format("%H:%M").to_string())
|
||||||
|
.justify(Justification::Right)
|
||||||
|
.css_classes(["time"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
timeout_add_local(Duration::from_secs(1), {
|
||||||
|
let time = time.clone();
|
||||||
|
|
||||||
|
move || {
|
||||||
|
time.set_label(&Local::now().format("%H:%M").to_string());
|
||||||
|
|
||||||
|
ControlFlow::Continue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fixed.put(&time, 432.0, 4.0);
|
||||||
|
fixed.set_halign(Align::End);
|
||||||
|
|
||||||
|
widget_box_overlay.add_overlay(&fixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget_box_overlay.set_child(Some(&widget_box));
|
||||||
|
|
||||||
|
widget_box_overlay
|
||||||
|
}
|
427
src/chat/gui/preferences.rs
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use adw::gdk::Display;
|
||||||
|
use adw::glib::clone;
|
||||||
|
use adw::glib::{self};
|
||||||
|
use adw::prelude::*;
|
||||||
|
use adw::Application;
|
||||||
|
use libadwaita::gtk::Adjustment;
|
||||||
|
use libadwaita::{
|
||||||
|
self as adw, ActionRow, ButtonRow, EntryRow, PreferencesDialog, PreferencesGroup,
|
||||||
|
PreferencesPage, SpinRow, SwitchRow,
|
||||||
|
};
|
||||||
|
|
||||||
|
use adw::gtk;
|
||||||
|
use gtk::Button;
|
||||||
|
|
||||||
|
use crate::chat::{
|
||||||
|
config::{get_config_path, Config},
|
||||||
|
ctx::Context,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{try_save_config, update_window_title};
|
||||||
|
|
||||||
|
pub fn open_settings(ctx: Arc<Context>, app: &Application) {
|
||||||
|
let dialog = PreferencesDialog::builder().build();
|
||||||
|
|
||||||
|
let page = PreferencesPage::builder()
|
||||||
|
.title("General")
|
||||||
|
.icon_name("avatar-default-symbolic")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let group = PreferencesGroup::builder()
|
||||||
|
.title("User Profile")
|
||||||
|
.description("Profile preferences")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Name preference
|
||||||
|
|
||||||
|
let name = EntryRow::builder()
|
||||||
|
.title("Name")
|
||||||
|
.text(ctx.config(|o| o.name.clone()).unwrap_or_default())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&name);
|
||||||
|
|
||||||
|
// Avatar preference
|
||||||
|
|
||||||
|
let avatar = EntryRow::builder()
|
||||||
|
.title("Avatar Link")
|
||||||
|
.text(ctx.config(|o| o.avatar.clone()).unwrap_or_default())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&avatar);
|
||||||
|
|
||||||
|
page.add(&group);
|
||||||
|
|
||||||
|
let group = PreferencesGroup::builder()
|
||||||
|
.title("Server")
|
||||||
|
.description("Connection preferences")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Host preference
|
||||||
|
|
||||||
|
let host = EntryRow::builder()
|
||||||
|
.title("Host")
|
||||||
|
.text(ctx.config(|o| o.host.clone()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&host);
|
||||||
|
|
||||||
|
// Messages limit preference
|
||||||
|
|
||||||
|
let messages_limit = SpinRow::builder()
|
||||||
|
.title("Messages limit")
|
||||||
|
.adjustment(
|
||||||
|
&Adjustment::builder()
|
||||||
|
.lower(1.0)
|
||||||
|
.upper(1048576.0)
|
||||||
|
.page_increment(10.0)
|
||||||
|
.step_increment(10.0)
|
||||||
|
.value(ctx.config(|o| o.max_messages) as f64)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&messages_limit);
|
||||||
|
|
||||||
|
// Update interval preference
|
||||||
|
|
||||||
|
let update_interval = SpinRow::builder()
|
||||||
|
.title("Update interval")
|
||||||
|
.subtitle("In milliseconds")
|
||||||
|
.adjustment(
|
||||||
|
&Adjustment::builder()
|
||||||
|
.lower(10.0)
|
||||||
|
.upper(1048576.0)
|
||||||
|
.page_increment(10.0)
|
||||||
|
.step_increment(10.0)
|
||||||
|
.value(ctx.config(|o| o.update_time) as f64)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&update_interval);
|
||||||
|
|
||||||
|
// Update interval OOF preference
|
||||||
|
|
||||||
|
let update_interval_oof = SpinRow::builder()
|
||||||
|
.title("Update interval when unfocused")
|
||||||
|
.subtitle("In milliseconds")
|
||||||
|
.adjustment(
|
||||||
|
&Adjustment::builder()
|
||||||
|
.lower(10.0)
|
||||||
|
.upper(1048576.0)
|
||||||
|
.page_increment(10.0)
|
||||||
|
.step_increment(10.0)
|
||||||
|
.value(ctx.config(|o| o.oof_update_time) as f64)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&update_interval_oof);
|
||||||
|
|
||||||
|
page.add(&group);
|
||||||
|
|
||||||
|
let group = PreferencesGroup::builder()
|
||||||
|
.title("Config")
|
||||||
|
.description("Configuration tools")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let display = Display::default().unwrap();
|
||||||
|
let clipboard = display.clipboard();
|
||||||
|
|
||||||
|
let config_path = ActionRow::builder()
|
||||||
|
.title("Config path")
|
||||||
|
.subtitle(get_config_path().to_string_lossy())
|
||||||
|
.css_classes(["property", "monospace"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let config_path_copy = Button::from_icon_name("edit-copy-symbolic");
|
||||||
|
|
||||||
|
// config_path_copy.set_css_classes(&["circular"]);
|
||||||
|
config_path_copy.set_margin_top(10);
|
||||||
|
config_path_copy.set_margin_bottom(10);
|
||||||
|
config_path_copy.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
clipboard,
|
||||||
|
move |_| {
|
||||||
|
if let Some(text) = get_config_path().to_str() {
|
||||||
|
clipboard.set_text(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
config_path.add_suffix(&config_path_copy);
|
||||||
|
config_path.set_activatable(false);
|
||||||
|
|
||||||
|
group.add(&config_path);
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
|
||||||
|
let reset_button = ButtonRow::builder().title("Reset all").build();
|
||||||
|
|
||||||
|
reset_button.connect_activated(clone!(
|
||||||
|
#[weak]
|
||||||
|
ctx,
|
||||||
|
#[weak]
|
||||||
|
app,
|
||||||
|
#[weak]
|
||||||
|
dialog,
|
||||||
|
move |_| {
|
||||||
|
dialog.close();
|
||||||
|
let config = Config::default();
|
||||||
|
ctx.set_config(&config);
|
||||||
|
try_save_config(get_config_path(), &config);
|
||||||
|
open_settings(ctx, &app);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
group.add(&reset_button);
|
||||||
|
|
||||||
|
page.add(&group);
|
||||||
|
|
||||||
|
dialog.add(&page);
|
||||||
|
|
||||||
|
let page = PreferencesPage::builder()
|
||||||
|
.title("Protocol")
|
||||||
|
.icon_name("network-wired-symbolic")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let group = PreferencesGroup::builder()
|
||||||
|
.title("Network")
|
||||||
|
.description("Network preferences")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Proxy preference
|
||||||
|
|
||||||
|
let proxy = EntryRow::builder()
|
||||||
|
.title("Socks proxy")
|
||||||
|
.text(ctx.config(|o| o.proxy.clone()).unwrap_or_default())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&proxy);
|
||||||
|
|
||||||
|
// Max avatar size preference
|
||||||
|
|
||||||
|
let max_avatar_size = SpinRow::builder()
|
||||||
|
.title("Max avatar size")
|
||||||
|
.subtitle("Maximum avatar size in bytes")
|
||||||
|
.adjustment(
|
||||||
|
&Adjustment::builder()
|
||||||
|
.lower(0.0)
|
||||||
|
.upper(1074790400.0)
|
||||||
|
.page_increment(1024.0)
|
||||||
|
.step_increment(1024.0)
|
||||||
|
.value(ctx.config(|o| o.max_avatar_size) as f64)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&max_avatar_size);
|
||||||
|
|
||||||
|
page.add(&group);
|
||||||
|
|
||||||
|
let group = PreferencesGroup::builder()
|
||||||
|
.title("Protocol")
|
||||||
|
.description("Rac protocol preferences")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Message format preference
|
||||||
|
|
||||||
|
let message_format = EntryRow::builder()
|
||||||
|
.title("Message format")
|
||||||
|
.text(ctx.config(|o| o.message_format.clone()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&message_format);
|
||||||
|
|
||||||
|
page.add(&group);
|
||||||
|
|
||||||
|
// Hide IP preference
|
||||||
|
|
||||||
|
let hide_my_ip = SwitchRow::builder()
|
||||||
|
.title("Hide IP")
|
||||||
|
.subtitle("Hides only for clRAC and other dummy clients")
|
||||||
|
.active(ctx.config(|o| o.hide_my_ip))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&hide_my_ip);
|
||||||
|
|
||||||
|
// Chunked reading preference
|
||||||
|
|
||||||
|
let chunked_reading = SwitchRow::builder()
|
||||||
|
.title("Chunked reading")
|
||||||
|
.subtitle("Read messages in chunks (less traffic usage, less compatibility)")
|
||||||
|
.active(ctx.config(|o| o.chunked_enabled))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&chunked_reading);
|
||||||
|
|
||||||
|
// Enable commands preference
|
||||||
|
|
||||||
|
let enable_commands = SwitchRow::builder()
|
||||||
|
.title("Enable commands")
|
||||||
|
.subtitle("Enable slash commands (eg. /login) on client-side")
|
||||||
|
.active(ctx.config(|o| o.commands_enabled))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&enable_commands);
|
||||||
|
|
||||||
|
page.add(&group);
|
||||||
|
|
||||||
|
dialog.add(&page);
|
||||||
|
|
||||||
|
let page = PreferencesPage::builder()
|
||||||
|
.title("Interface")
|
||||||
|
.icon_name("applications-graphics-symbolic")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let group = PreferencesGroup::builder()
|
||||||
|
.title("Messages")
|
||||||
|
.description("Messages render preferences")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Debug logs preference
|
||||||
|
|
||||||
|
let debug_logs = SwitchRow::builder()
|
||||||
|
.title("Debug logs")
|
||||||
|
.subtitle("Print debug logs to the chat")
|
||||||
|
.active(ctx.config(|o| o.debug_logs))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&debug_logs);
|
||||||
|
|
||||||
|
// Show IPs preference
|
||||||
|
|
||||||
|
let show_ips = SwitchRow::builder()
|
||||||
|
.title("Show IPs")
|
||||||
|
.subtitle("Show authors IP addresses if possible")
|
||||||
|
.active(ctx.config(|o| o.show_other_ip))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&show_ips);
|
||||||
|
|
||||||
|
// Format messages preference
|
||||||
|
|
||||||
|
let format_messages = SwitchRow::builder()
|
||||||
|
.title("Format messages")
|
||||||
|
.subtitle("Disable to see raw messages")
|
||||||
|
.active(ctx.config(|o| o.formatting_enabled))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&format_messages);
|
||||||
|
|
||||||
|
// Show avatars preference
|
||||||
|
|
||||||
|
let show_avatars = SwitchRow::builder()
|
||||||
|
.title("Show avatars")
|
||||||
|
.subtitle("Enables new messages UI")
|
||||||
|
.active(ctx.config(|o| o.new_ui_enabled))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&show_avatars);
|
||||||
|
page.add(&group);
|
||||||
|
|
||||||
|
let group = PreferencesGroup::builder()
|
||||||
|
.title("Interface")
|
||||||
|
.description("General interface preferences (restart after changing)")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Remove GUI shit preference
|
||||||
|
|
||||||
|
let remove_gui_shit = SwitchRow::builder()
|
||||||
|
.title("Remove GUI shit")
|
||||||
|
.subtitle("Removes calendar, konata and clock")
|
||||||
|
.active(ctx.config(|o| o.remove_gui_shit))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&remove_gui_shit);
|
||||||
|
|
||||||
|
// Konata size preference
|
||||||
|
|
||||||
|
let konata_size = SpinRow::builder()
|
||||||
|
.title("Konata size")
|
||||||
|
.subtitle("Set konata size percent")
|
||||||
|
.adjustment(
|
||||||
|
&Adjustment::builder()
|
||||||
|
.lower(0.0)
|
||||||
|
.upper(200.0)
|
||||||
|
.page_increment(10.0)
|
||||||
|
.step_increment(10.0)
|
||||||
|
.value(ctx.config(|o| o.konata_size) as f64)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&konata_size);
|
||||||
|
|
||||||
|
// Enable notifications preference
|
||||||
|
|
||||||
|
let enable_notifications = SwitchRow::builder()
|
||||||
|
.title("Enable notifications")
|
||||||
|
.subtitle("Send notifications on chat and system messages")
|
||||||
|
.active(ctx.config(|o| o.notifications_enabled))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
group.add(&enable_notifications);
|
||||||
|
page.add(&group);
|
||||||
|
|
||||||
|
dialog.add(&page);
|
||||||
|
|
||||||
|
dialog.connect_closed(move |_| {
|
||||||
|
let old_config = ctx.config.read().unwrap().clone();
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
host: host.text().to_string(),
|
||||||
|
name: {
|
||||||
|
let name = name.text().to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
let avatar = avatar.text().to_string();
|
||||||
|
|
||||||
|
if avatar.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(avatar)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message_format: message_format.text().to_string(),
|
||||||
|
update_time: update_interval.value() as usize,
|
||||||
|
oof_update_time: update_interval_oof.value() as usize,
|
||||||
|
konata_size: konata_size.value() as usize,
|
||||||
|
max_messages: messages_limit.value() as usize,
|
||||||
|
max_avatar_size: max_avatar_size.value() as u64,
|
||||||
|
hide_my_ip: hide_my_ip.is_active(),
|
||||||
|
remove_gui_shit: remove_gui_shit.is_active(),
|
||||||
|
show_other_ip: show_ips.is_active(),
|
||||||
|
chunked_enabled: chunked_reading.is_active(),
|
||||||
|
formatting_enabled: format_messages.is_active(),
|
||||||
|
commands_enabled: enable_commands.is_active(),
|
||||||
|
notifications_enabled: enable_notifications.is_active(),
|
||||||
|
new_ui_enabled: show_avatars.is_active(),
|
||||||
|
debug_logs: debug_logs.is_active(),
|
||||||
|
proxy: {
|
||||||
|
let proxy = proxy.text().to_string();
|
||||||
|
|
||||||
|
if proxy.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(proxy)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
servers: old_config.servers,
|
||||||
|
};
|
||||||
|
ctx.set_config(&config);
|
||||||
|
try_save_config(get_config_path(), &config);
|
||||||
|
update_window_title(ctx.clone());
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.present(app.active_window().as_ref());
|
||||||
|
}
|
32
src/chat/gui/styles/style.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/* .send-button, .send-text { border-radius: 0; } */
|
||||||
|
.calendar {
|
||||||
|
transform: scale(0.6);
|
||||||
|
margin: -35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-box {
|
||||||
|
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.20);
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.20);
|
||||||
|
min-height: 121px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-avatar {
|
||||||
|
border-radius: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now made with GTK Pango Markup */
|
||||||
|
|
||||||
|
.message-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .message-name-green { color: #70fa7a; }
|
||||||
|
.message-name-red { color: #fa7070; }
|
||||||
|
.message-name-magenta { color: #da70fa; }
|
||||||
|
.message-name-cyan { color: #70fadc; } */
|
51
src/chat/gui/widgets/imp.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use libadwaita::{glib, gtk};
|
||||||
|
|
||||||
|
use glib::object::ObjectExt;
|
||||||
|
use gtk::{prelude::LayoutManagerExt, subclass::prelude::*, BoxLayout};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CustomLayout {
|
||||||
|
box_layout: BoxLayout,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CustomLayout {
|
||||||
|
fn default() -> Self {
|
||||||
|
CustomLayout {
|
||||||
|
box_layout: BoxLayout::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.spacing(5)
|
||||||
|
.build(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for CustomLayout {
|
||||||
|
const NAME: &'static str = "CustomLayout";
|
||||||
|
type Type = super::CustomLayout;
|
||||||
|
type ParentType = gtk::LayoutManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for CustomLayout {
|
||||||
|
fn signals() -> &'static [glib::subclass::Signal] {
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
static SIGNALS: OnceLock<Vec<glib::subclass::Signal>> = OnceLock::new();
|
||||||
|
|
||||||
|
SIGNALS.get_or_init(|| vec![glib::subclass::Signal::builder("size-changed").build()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl LayoutManagerImpl for CustomLayout {
|
||||||
|
fn allocate(&self, widget: >k::Widget, width: i32, height: i32, baseline: i32) {
|
||||||
|
self.obj().emit_by_name::<()>("size-changed", &[]);
|
||||||
|
self.box_layout.allocate(widget, width, height, baseline)
|
||||||
|
}
|
||||||
|
fn measure(
|
||||||
|
&self,
|
||||||
|
widget: >k::Widget,
|
||||||
|
orientation: gtk::Orientation,
|
||||||
|
for_size: i32,
|
||||||
|
) -> (i32, i32, i32, i32) {
|
||||||
|
self.box_layout.measure(widget, orientation, for_size)
|
||||||
|
}
|
||||||
|
}
|
15
src/chat/gui/widgets/mod.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
mod imp;
|
||||||
|
|
||||||
|
use libadwaita::gtk;
|
||||||
|
use libadwaita::gtk::glib;
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct CustomLayout(ObjectSubclass<imp::CustomLayout>)
|
||||||
|
@extends gtk::LayoutManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CustomLayout {
|
||||||
|
fn default() -> Self {
|
||||||
|
glib::Object::new()
|
||||||
|
}
|
||||||
|
}
|
@ -67,9 +67,9 @@ pub fn sanitize_text(input: &str) -> String {
|
|||||||
|
|
||||||
#[cfg(feature = "gtk")]
|
#[cfg(feature = "gtk")]
|
||||||
pub fn add_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
|
pub fn add_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
|
||||||
for i in message.split("\n").map(|o| o.to_string()) {
|
let messages: Vec<String> = message.split("\n").map(|o| o.to_string()).collect();
|
||||||
print_message(ctx.clone(), i)?;
|
ctx.add_message(ctx.config(|o| o.max_messages), messages.clone());
|
||||||
}
|
add_chat_messages(ctx.clone(), messages);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
.send-button, .send-text { border-radius: 0; }
|
|
||||||
.calendar {
|
|
||||||
transform: scale(0.6);
|
|
||||||
margin: -35px;
|
|
||||||
}
|
|
||||||
.widget_box {
|
|
||||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.20);
|
|
||||||
border-bottom: 2px solid rgba(0, 0, 0, 0.20);
|
|
||||||
min-height: 121px;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
font-size: 20px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-avatar {
|
|
||||||
border-radius: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Now made with GTK Pango Markup */
|
|
||||||
|
|
||||||
.message-name { font-weight: bold; }
|
|
||||||
|
|
||||||
/* .message-name-green { color: #70fa7a; }
|
|
||||||
.message-name-red { color: #fa7070; }
|
|
||||||
.message-name-magenta { color: #da70fa; }
|
|
||||||
.message-name-cyan { color: #70fadc; } */
|
|