Add a /sticker plugin
This plugin currently uploads the selected sticker every time, to the HTTP File Upload service of the server (see XEP-0363), a future optimisation would be to use XEP-0231 instead, for better caching on the recipient side. It relies on a helper tool to select the wanted sticker inside the pack, a sample one is provided in tools/sticker-picker/, but it is not built by default.
This commit is contained in:
parent
9735b6d6dc
commit
d35c0564b3
7 changed files with 325 additions and 0 deletions
|
@ -1,4 +1,5 @@
|
|||
recursive-include doc/source *
|
||||
recursive-include tools
|
||||
include data/poezio.1
|
||||
include data/io.poez.Poezio.appdata.xml
|
||||
include data/io.poez.Poezio.desktop
|
||||
|
|
|
@ -211,6 +211,11 @@ Plugin index
|
|||
|
||||
Adds convenient aliases to /status (/away, etc).
|
||||
|
||||
Sticker
|
||||
:ref:`Documentation <sticker-plugin>`
|
||||
|
||||
Opens a graphical sticker picker and sends the selected one.
|
||||
|
||||
Tell
|
||||
:ref:`Documentation <tell-plugin>`
|
||||
|
||||
|
@ -342,6 +347,7 @@ Plugin index
|
|||
simple_notify
|
||||
spam
|
||||
status
|
||||
sticker
|
||||
tell
|
||||
time_marker
|
||||
uptime
|
||||
|
|
6
doc/source/plugins/sticker.rst
Normal file
6
doc/source/plugins/sticker.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
.. _sticker-plugin:
|
||||
|
||||
Sticker
|
||||
=======
|
||||
|
||||
.. automodule:: sticker
|
97
plugins/sticker.py
Normal file
97
plugins/sticker.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
'''
|
||||
This plugin lets the user select and send a sticker from a pack of stickers.
|
||||
|
||||
The protocol used here is based on XEP-0363 and XEP-0066, while a future
|
||||
version may use XEP-0449 instead.
|
||||
|
||||
Command
|
||||
-------
|
||||
|
||||
.. glossary::
|
||||
/sticker
|
||||
**Usage:** ``/sticker <pack>``
|
||||
|
||||
Opens a picker tool, and send the sticker which has been selected.
|
||||
|
||||
Configuration options
|
||||
---------------------
|
||||
|
||||
.. glossary::
|
||||
sticker_picker
|
||||
**Default:** ``poezio-sticker-picker``
|
||||
|
||||
The command to invoke as a sticker picker. A sample one is provided in
|
||||
tools/sticker-picker.
|
||||
|
||||
stickers_dir
|
||||
**Default:** ``XDG_DATA_HOME/poezio/stickers``
|
||||
|
||||
The directory under which the sticker packs can be found.
|
||||
'''
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
from poezio import xdg
|
||||
from poezio.plugin import BasePlugin
|
||||
from poezio.config import config
|
||||
from poezio.decorators import command_args_parser
|
||||
from poezio.core.structs import Completion
|
||||
from pathlib import Path
|
||||
from asyncio.subprocess import PIPE, DEVNULL
|
||||
|
||||
class Plugin(BasePlugin):
|
||||
dependencies = {'upload'}
|
||||
|
||||
def init(self):
|
||||
# The command to use as a picker helper.
|
||||
self.picker_command = config.getstr('sticker_picker') or 'poezio-sticker-picker'
|
||||
|
||||
# Select and create the stickers directory.
|
||||
directory = config.getstr('stickers_dir')
|
||||
if directory:
|
||||
self.directory = Path(directory).expanduser()
|
||||
else:
|
||||
self.directory = xdg.DATA_HOME / 'stickers'
|
||||
self.directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.upload = self.refs['upload']
|
||||
self.api.add_command('sticker', self.command_sticker,
|
||||
usage='<sticker pack>',
|
||||
short='Send a sticker',
|
||||
help='Send a sticker, with a helper GUI sticker picker',
|
||||
completion=self.completion_sticker)
|
||||
|
||||
def command_sticker(self, pack):
|
||||
'''
|
||||
Sends a sticker
|
||||
'''
|
||||
if not pack:
|
||||
self.api.information('Missing sticker pack argument.', 'Error')
|
||||
return
|
||||
async def run_command(tab, path: Path):
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
self.picker_command, path, stdout=PIPE, stderr=PIPE)
|
||||
sticker, stderr = await process.communicate()
|
||||
except FileNotFoundError as err:
|
||||
self.api.information('Failed to launch the sticker picker: %s' % err, 'Error')
|
||||
return
|
||||
else:
|
||||
if process.returncode != 0:
|
||||
self.api.information('Sticker picker failed: %s' % stderr.decode(), 'Error')
|
||||
return
|
||||
if sticker:
|
||||
filename = sticker.decode().rstrip()
|
||||
self.api.information('Sending sticker %s' % filename, 'Info')
|
||||
await self.upload.send_upload(path / filename, tab)
|
||||
tab = self.api.current_tab()
|
||||
path = self.directory / pack
|
||||
asyncio.create_task(run_command(tab, path))
|
||||
|
||||
def completion_sticker(self, the_input):
|
||||
'''
|
||||
Completion for /sticker
|
||||
'''
|
||||
txt = the_input.get_text()[9:]
|
||||
directories = [directory.name for directory in self.directory.glob(txt + '*')]
|
||||
return Completion(the_input.auto_completion, directories, quotify=False)
|
16
tools/sticker-picker/Cargo.toml
Normal file
16
tools/sticker-picker/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "poezio-sticker-picker"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>"]
|
||||
license = "Zlib"
|
||||
description = "Helper tool for selecting a sticker inside a pack"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
gtk = { package = "gtk4", version = "0.4", features = ["v4_6"] }
|
||||
gdk = { package = "gdk4", version = "0.4", features = ["v4_6"] }
|
||||
glib = "0.15"
|
||||
gio = "0.15"
|
||||
once_cell = "1.9.0"
|
93
tools/sticker-picker/src/main.rs
Normal file
93
tools/sticker-picker/src/main.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
// This file is part of Poezio.
|
||||
//
|
||||
// Poezio is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the zlib license. See the COPYING file.
|
||||
|
||||
mod sticker;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use sticker::StickerType as Sticker;
|
||||
|
||||
fn main() {
|
||||
let app = gtk::Application::builder()
|
||||
.application_id("io.poez.StickerPicker")
|
||||
.flags(gio::ApplicationFlags::HANDLES_OPEN)
|
||||
.build();
|
||||
|
||||
let quit = gio::SimpleAction::new("quit", None);
|
||||
app.set_accels_for_action("app.quit", &["<Control>q"]);
|
||||
app.add_action(&quit);
|
||||
quit.connect_activate(glib::clone!(@weak app => move |_, _| app.quit()));
|
||||
|
||||
app.connect_open(move |app, directories, _| {
|
||||
let path = match directories {
|
||||
[directory] => directory.path().unwrap(),
|
||||
_ => {
|
||||
eprintln!("Only a single directory is allowed!");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.default_width(1280)
|
||||
.default_height(720)
|
||||
.title("Poezio Sticker Picker")
|
||||
.build();
|
||||
|
||||
let sw = gtk::ScrolledWindow::builder()
|
||||
.has_frame(true)
|
||||
.hscrollbar_policy(gtk::PolicyType::Always)
|
||||
.vscrollbar_policy(gtk::PolicyType::Always)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
window.set_child(Some(&sw));
|
||||
|
||||
let store = gio::ListStore::new(Sticker::static_type());
|
||||
|
||||
for dir_entry in std::fs::read_dir(path).unwrap() {
|
||||
let dir_entry = dir_entry.unwrap();
|
||||
let file_name = dir_entry.file_name().into_string().unwrap();
|
||||
let sticker = Sticker::new(file_name, &dir_entry.path());
|
||||
store.append(&sticker);
|
||||
}
|
||||
|
||||
let factory = gtk::SignalListItemFactory::new();
|
||||
factory.connect_setup(|_, item| {
|
||||
let picture = gtk::Picture::builder()
|
||||
.alternative_text("Sticker")
|
||||
.can_shrink(false)
|
||||
.build();
|
||||
item.set_child(Some(&picture));
|
||||
});
|
||||
factory.connect_bind(|_, list_item| {
|
||||
if let Some(child) = list_item.child() {
|
||||
if let Some(item) = list_item.item() {
|
||||
let picture: gtk::Picture = child.downcast().unwrap();
|
||||
let sticker: Sticker = item.downcast().unwrap();
|
||||
picture.set_paintable(sticker.texture().as_ref());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let selection = gtk::SingleSelection::new(Some(&store));
|
||||
let grid_view = gtk::GridView::builder()
|
||||
.single_click_activate(true)
|
||||
.model(&selection)
|
||||
.factory(&factory)
|
||||
.build();
|
||||
grid_view.connect_activate(move |_, position| {
|
||||
let item = store.item(position).unwrap();
|
||||
let sticker: Sticker = item.downcast().unwrap();
|
||||
if let Some(filename) = sticker.filename() {
|
||||
println!("{}", filename);
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
sw.set_child(Some(&grid_view));
|
||||
|
||||
window.show();
|
||||
});
|
||||
|
||||
app.run();
|
||||
}
|
106
tools/sticker-picker/src/sticker.rs
Normal file
106
tools/sticker-picker/src/sticker.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
// This file is part of Poezio.
|
||||
//
|
||||
// Poezio is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the zlib license. See the COPYING file.
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Sticker {
|
||||
filename: RefCell<Option<String>>,
|
||||
texture: RefCell<Option<gdk::Texture>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Sticker {
|
||||
const NAME: &'static str = "Sticker";
|
||||
type Type = StickerType;
|
||||
}
|
||||
|
||||
impl ObjectImpl for Sticker {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
use once_cell::sync::Lazy;
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecString::new(
|
||||
"filename",
|
||||
"Filename",
|
||||
"Filename",
|
||||
None,
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
glib::ParamSpecObject::new(
|
||||
"texture",
|
||||
"Texture",
|
||||
"Texture",
|
||||
gdk::Texture::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
]
|
||||
});
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(
|
||||
&self,
|
||||
_obj: &StickerType,
|
||||
_id: usize,
|
||||
value: &glib::Value,
|
||||
pspec: &glib::ParamSpec,
|
||||
) {
|
||||
match pspec.name() {
|
||||
"filename" => {
|
||||
let filename = value.get().unwrap();
|
||||
self.filename.replace(filename);
|
||||
}
|
||||
"texture" => {
|
||||
let texture = value.get().unwrap();
|
||||
self.texture.replace(texture);
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _obj: &StickerType, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"filename" => self.filename.borrow().to_value(),
|
||||
"texture" => self.texture.borrow().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct StickerType(ObjectSubclass<Sticker>);
|
||||
}
|
||||
|
||||
impl StickerType {
|
||||
pub fn new(filename: String, path: &Path) -> StickerType {
|
||||
let texture = gdk::Texture::from_filename(path).unwrap();
|
||||
glib::Object::new(&[("filename", &filename), ("texture", &texture)])
|
||||
.expect("Failed to create Sticker")
|
||||
}
|
||||
|
||||
pub fn filename(&self) -> Option<String> {
|
||||
let imp = self.imp();
|
||||
let filename = imp.filename.borrow();
|
||||
if let Some(filename) = filename.as_ref() {
|
||||
Some(filename.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn texture(&self) -> Option<gdk::Texture> {
|
||||
let imp = self.imp();
|
||||
let texture = imp.texture.borrow();
|
||||
if let Some(texture) = texture.as_ref() {
|
||||
Some(texture.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue