commit
595edab828
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -329,7 +329,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "espanso"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "espanso"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||
license = "GPL-3.0"
|
||||
description = "Cross-platform Text Expander written in Rust"
|
||||
|
|
113
README.md
113
README.md
|
@ -9,6 +9,8 @@
|
|||
|
||||
![example](images/example.gif)
|
||||
|
||||
Visit the [espanso website](https://espanso.org).
|
||||
|
||||
#### What is a Text Expander?
|
||||
|
||||
A *text expander* is a program that detects when you type
|
||||
|
@ -30,111 +32,13 @@ ___
|
|||
* **Custom scripts** support
|
||||
* **Shell commands** support
|
||||
* **App-specific** configurations
|
||||
* Expandable with **packages**
|
||||
* Built-in **package manager** for [espanso hub](https://hub.espanso.org/)
|
||||
* File based configuration
|
||||
|
||||
## Table of contents
|
||||
## Get Started
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Windows](#install-windows)
|
||||
- [Linux](#install-linux)
|
||||
- [macOS](#install-macos)
|
||||
- [Usage](#usage)
|
||||
- [FAQ](#faq)
|
||||
- [Donations](#donations)
|
||||
- [License](#license)
|
||||
|
||||
## Installation
|
||||
|
||||
### <a name="install-windows"></a>Windows
|
||||
|
||||
The installation on Windows is pretty straightforward, navigate to the
|
||||
[Release](https://github.com/federico-terzi/espanso/releases) page and
|
||||
download the latest installer ( usually named like
|
||||
`espanso-win-0.1.0.exe` ).
|
||||
|
||||
Because espanso is not digitally signed, you may experience a warning from
|
||||
Windows Smartscreen. In this case, just click on "More info" (1) and then
|
||||
on "Run anyway" (2), as shown in the picture:
|
||||
|
||||
![Windows Smartscreen](images/windows-smartscreen.png)
|
||||
|
||||
If you completed the installation procedure, you should have espanso running.
|
||||
A good way to find out is by going on any text field and typing `:espanso`.
|
||||
You should see "Hi there!" appear.
|
||||
|
||||
### <a name="install-linux"></a>Linux
|
||||
|
||||
TODO
|
||||
|
||||
### <a name="install-macos"></a>MacOS
|
||||
|
||||
The easiest way to install espanso on macOS using the [Homebrew](https://brew.sh/)
|
||||
package manager, but you can also do it manually.
|
||||
|
||||
#### Using Homebrew
|
||||
|
||||
The first thing to do is to add the official espanso *tap* to Homebrew with
|
||||
the following command:
|
||||
|
||||
```
|
||||
brew tap federico-terzi/espanso
|
||||
```
|
||||
|
||||
Then you can install espanso with:
|
||||
|
||||
```
|
||||
brew install espanso
|
||||
```
|
||||
|
||||
To make sure that espanso was correctly installed, you can open a terminal and type:
|
||||
|
||||
```
|
||||
espanso --version
|
||||
```
|
||||
|
||||
At this point, you have to [Enable Accessibility](#enabling-accessibility) to use espanso.
|
||||
|
||||
#### Enabling Accessibility
|
||||
|
||||
Because espanso uses the macOS [Accessibility API](https://developer.apple.com/library/archive/documentation/Accessibility/Conceptual/AccessibilityMacOSX/)
|
||||
to work, you need to authorize it using the following procedure:
|
||||
|
||||
Open a terminal and type the command:
|
||||
|
||||
```
|
||||
espanso install
|
||||
```
|
||||
|
||||
A dialog should show up, click on "Open System Preferences", as shown here:
|
||||
|
||||
![Accessibility Prompt](images/accessibility-prompt.png)
|
||||
|
||||
Then, in the "Privacy" panel click on the Lock icon (1) to enable edits and
|
||||
then check "espanso" (2), as shown in the picture:
|
||||
|
||||
![Accessibility Settings](images/accessibility-macos-enable.png)
|
||||
|
||||
Now open the terminal again and type:
|
||||
|
||||
```
|
||||
espanso install
|
||||
```
|
||||
|
||||
If everything goes well, you should see the espanso icon appear in the status bar:
|
||||
|
||||
![macOS status bar icon](images/espanso-icon-macos-statusbar.png)
|
||||
|
||||
If you now type `:espanso` in any text field, you should see "Hi there!" appear!
|
||||
|
||||
## Usage
|
||||
|
||||
TODO
|
||||
|
||||
## FAQ
|
||||
|
||||
#### How does espanso work?
|
||||
|
||||
TODO
|
||||
Visit the [official documentation](https://espanso.org/docs/).
|
||||
|
||||
## Donations
|
||||
|
||||
|
@ -144,6 +48,11 @@ please consider making a small donation, it really helps :)
|
|||
|
||||
[![Donate with PayPal](images/donate.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FHNLR5DRS267E&source=url)
|
||||
|
||||
## Remarks
|
||||
|
||||
* Special thanks to the [ModifyPath](https://www.legroom.net/software/modpath)
|
||||
script, used by espanso to improve the Windows installer.
|
||||
|
||||
## License
|
||||
|
||||
espanso was created by [Federico Terzi](http://federicoterzi.com)
|
||||
|
|
|
@ -30,7 +30,7 @@ pub fn check_dependencies() -> bool {
|
|||
let status = Command::new("notify-send")
|
||||
.arg("-v")
|
||||
.output();
|
||||
if let Err(_) = status {
|
||||
if status.is_err() {
|
||||
println!("Error: 'notify-send' command is needed for espanso to work correctly, please install it.");
|
||||
result = false;
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ pub fn check_dependencies() -> bool {
|
|||
let status = Command::new("xclip")
|
||||
.arg("-version")
|
||||
.output();
|
||||
if let Err(_) = status {
|
||||
if status.is_err() {
|
||||
println!("Error: 'xclip' command is needed for espanso to work correctly, please install it.");
|
||||
result = false;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ use walkdir::WalkDir;
|
|||
|
||||
pub(crate) mod runtime;
|
||||
|
||||
// TODO: add documentation link
|
||||
const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yml");
|
||||
|
||||
const DEFAULT_CONFIG_FILE_NAME : &str = "default.yml";
|
||||
|
@ -162,7 +161,7 @@ impl Configs {
|
|||
let mut contents = String::new();
|
||||
let res = file.read_to_string(&mut contents);
|
||||
|
||||
if let Err(_) = res {
|
||||
if res.is_err() {
|
||||
return Err(ConfigLoadError::UnableToReadFile)
|
||||
}
|
||||
|
||||
|
@ -187,7 +186,7 @@ impl Configs {
|
|||
});
|
||||
let parent_matches : Vec<Match> = self.matches.iter().filter(|&m| {
|
||||
!trigger_set.contains(&m.trigger)
|
||||
}).map(|m| m.clone()).collect();
|
||||
}).cloned().collect();
|
||||
|
||||
merged_matches.extend(parent_matches);
|
||||
self.matches = merged_matches;
|
||||
|
@ -200,7 +199,7 @@ impl Configs {
|
|||
});
|
||||
let default_matches : Vec<Match> = default.matches.iter().filter(|&m| {
|
||||
!trigger_set.contains(&m.trigger)
|
||||
}).map(|m| m.clone()).collect();
|
||||
}).cloned().collect();
|
||||
|
||||
self.matches.extend(default_matches);
|
||||
}
|
||||
|
@ -326,7 +325,7 @@ impl ConfigSet {
|
|||
// Create the espanso dir if id doesn't exist
|
||||
let res = create_dir_all(espanso_dir.as_path());
|
||||
|
||||
if let Ok(_) = res {
|
||||
if res.is_ok() {
|
||||
let default_file = espanso_dir.join(DEFAULT_CONFIG_FILE_NAME);
|
||||
|
||||
// If config file does not exist, create one from template
|
||||
|
@ -358,13 +357,12 @@ impl ConfigSet {
|
|||
return ConfigSet::load(espanso_dir.as_path())
|
||||
}
|
||||
|
||||
return Err(ConfigLoadError::UnableToCreateDefaultConfig)
|
||||
Err(ConfigLoadError::UnableToCreateDefaultConfig)
|
||||
}
|
||||
|
||||
pub fn get_default_config_dir() -> PathBuf {
|
||||
let home_dir = dirs::home_dir().expect("Unable to get home directory");
|
||||
let espanso_dir = home_dir.join(".espanso");
|
||||
espanso_dir
|
||||
home_dir.join(".espanso")
|
||||
}
|
||||
|
||||
pub fn get_default_packages_dir() -> PathBuf {
|
||||
|
|
|
@ -27,7 +27,7 @@ use std::fs;
|
|||
use log::{info, error};
|
||||
use std::process::exit;
|
||||
|
||||
const STATUS_ICON_BINARY : &'static [u8] = include_bytes!("../res/mac/icon.png");
|
||||
const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png");
|
||||
|
||||
pub struct MacContext {
|
||||
pub send_channel: Sender<Event>
|
||||
|
@ -42,7 +42,7 @@ impl MacContext {
|
|||
if res == 0 {
|
||||
error!("Accessibility must be enabled to make espanso work on MacOS.");
|
||||
error!("Please allow espanso in the Security & Privacy panel, then restart espanso.");
|
||||
error!("For more information: "); // TODO: add documentation link
|
||||
error!("For more information: https://espanso.org/install/mac/");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ use std::{fs};
|
|||
use widestring::U16CString;
|
||||
use log::{info};
|
||||
|
||||
const BMP_BINARY : &'static [u8] = include_bytes!("../res/win/espanso.bmp");
|
||||
const ICO_BINARY : &'static [u8] = include_bytes!("../res/win/espanso.ico");
|
||||
const BMP_BINARY : &[u8] = include_bytes!("../res/win/espanso.bmp");
|
||||
const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico");
|
||||
|
||||
pub struct WindowsContext {
|
||||
send_channel: Sender<Event>,
|
||||
|
|
|
@ -55,7 +55,7 @@ impl <'a> EventManager for DefaultEventManager<'a> {
|
|||
}
|
||||
}
|
||||
},
|
||||
Err(_) => panic!("Broken event channel"),
|
||||
Err(e) => panic!("Broken event channel {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
src/main.rs
35
src/main.rs
|
@ -63,7 +63,7 @@ mod clipboard;
|
|||
mod extension;
|
||||
mod sysdaemon;
|
||||
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const LOG_FILE: &str = "espanso.log";
|
||||
|
||||
fn main() {
|
||||
|
@ -168,52 +168,52 @@ fn main() {
|
|||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("dump") {
|
||||
if matches.subcommand_matches("dump").is_some() {
|
||||
println!("{:#?}", config_set);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("detect") {
|
||||
if matches.subcommand_matches("detect").is_some() {
|
||||
detect_main();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("daemon") {
|
||||
if matches.subcommand_matches("daemon").is_some() {
|
||||
daemon_main(config_set);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("register") {
|
||||
if matches.subcommand_matches("register").is_some() {
|
||||
register_main(config_set);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("unregister") {
|
||||
if matches.subcommand_matches("unregister").is_some() {
|
||||
unregister_main(config_set);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("log") {
|
||||
if matches.subcommand_matches("log").is_some() {
|
||||
log_main();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("start") {
|
||||
if matches.subcommand_matches("start").is_some() {
|
||||
start_main(config_set);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("status") {
|
||||
if matches.subcommand_matches("status").is_some() {
|
||||
status_main();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("stop") {
|
||||
if matches.subcommand_matches("stop").is_some() {
|
||||
stop_main(config_set);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = matches.subcommand_matches("restart") {
|
||||
if matches.subcommand_matches("restart").is_some() {
|
||||
restart_main(config_set);
|
||||
return;
|
||||
}
|
||||
|
@ -236,7 +236,7 @@ fn main() {
|
|||
list_package_main(config_set, matches);
|
||||
return;
|
||||
}
|
||||
if let Some(_) = matches.subcommand_matches("refresh") {
|
||||
if matches.subcommand_matches("refresh").is_some() {
|
||||
update_index_main(config_set);
|
||||
return;
|
||||
}
|
||||
|
@ -244,6 +244,7 @@ fn main() {
|
|||
|
||||
// Defaults help print
|
||||
clap_instance.print_long_help().expect("Unable to print help");
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Daemon subcommand, start the event loop and spawn a background thread worker
|
||||
|
@ -524,22 +525,22 @@ fn detect_main() {
|
|||
|
||||
/// Send the given command to the espanso daemon
|
||||
fn cmd_main(config_set: ConfigSet, matches: &ArgMatches) {
|
||||
let command = if let Some(_) = matches.subcommand_matches("exit") {
|
||||
let command = if matches.subcommand_matches("exit").is_some() {
|
||||
Some(IPCCommand {
|
||||
id: String::from("exit"),
|
||||
payload: String::from(""),
|
||||
})
|
||||
}else if let Some(_) = matches.subcommand_matches("toggle") {
|
||||
}else if matches.subcommand_matches("toggle").is_some() {
|
||||
Some(IPCCommand {
|
||||
id: String::from("toggle"),
|
||||
payload: String::from(""),
|
||||
})
|
||||
}else if let Some(_) = matches.subcommand_matches("enable") {
|
||||
}else if matches.subcommand_matches("enable").is_some() {
|
||||
Some(IPCCommand {
|
||||
id: String::from("enable"),
|
||||
payload: String::from(""),
|
||||
})
|
||||
}else if let Some(_) = matches.subcommand_matches("disable") {
|
||||
}else if matches.subcommand_matches("disable").is_some() {
|
||||
Some(IPCCommand {
|
||||
id: String::from("disable"),
|
||||
payload: String::from(""),
|
||||
|
@ -747,7 +748,7 @@ fn acquire_lock() -> Option<File> {
|
|||
|
||||
let res = file.try_lock_exclusive();
|
||||
|
||||
if let Ok(_) = res {
|
||||
if res.is_ok() {
|
||||
return Some(file)
|
||||
}
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
|||
if m == config.toggle_key {
|
||||
let mut toggle_press_time = self.toggle_press_time.borrow_mut();
|
||||
if let Ok(elapsed) = toggle_press_time.elapsed() {
|
||||
if elapsed.as_millis() < config.toggle_interval as u128 {
|
||||
if elapsed.as_millis() < u128::from(config.toggle_interval) {
|
||||
self.toggle();
|
||||
|
||||
let is_enabled = self.is_enabled.borrow();
|
||||
|
|
|
@ -117,16 +117,14 @@ impl DefaultPackageManager {
|
|||
}else{
|
||||
started = true;
|
||||
}
|
||||
}else{
|
||||
if started {
|
||||
let caps = FIELD_REGEX.captures(&line);
|
||||
if let Some(caps) = caps {
|
||||
let property = caps.get(1);
|
||||
let value = caps.get(2);
|
||||
if property.is_some() && value.is_some() {
|
||||
fields.insert(property.unwrap().as_str().to_owned(),
|
||||
value.unwrap().as_str().to_owned());
|
||||
}
|
||||
}else if started {
|
||||
let caps = FIELD_REGEX.captures(&line);
|
||||
if let Some(caps) = caps {
|
||||
let property = caps.get(1);
|
||||
let value = caps.get(2);
|
||||
if property.is_some() && value.is_some() {
|
||||
fields.insert(property.unwrap().as_str().to_owned(),
|
||||
value.unwrap().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -161,7 +159,7 @@ impl DefaultPackageManager {
|
|||
return local_index.last_update
|
||||
}
|
||||
|
||||
return 0;
|
||||
0
|
||||
}
|
||||
|
||||
fn list_local_packages_names(&self) -> Vec<String> {
|
||||
|
@ -260,14 +258,14 @@ impl super::PackageManager for DefaultPackageManager {
|
|||
let readme_path = temp_package_dir.join("README.md");
|
||||
|
||||
let package = Self::parse_package_from_readme(&readme_path);
|
||||
if !package.is_some() {
|
||||
return Ok(InstallResult::UnableToParsePackageInfo); // TODO: test
|
||||
if package.is_none() {
|
||||
return Ok(InstallResult::UnableToParsePackageInfo);
|
||||
}
|
||||
let package = package.unwrap();
|
||||
|
||||
let source_dir = temp_package_dir.join(package.version);
|
||||
if !source_dir.exists() {
|
||||
return Ok(InstallResult::MissingPackageVersion); // TODO: test
|
||||
return Ok(InstallResult::MissingPackageVersion);
|
||||
}
|
||||
|
||||
let target_dir = &self.package_dir.join(name);
|
||||
|
@ -317,7 +315,7 @@ mod tests {
|
|||
use std::path::Path;
|
||||
use crate::package::PackageManager;
|
||||
use std::fs::{create_dir, create_dir_all};
|
||||
use crate::package::InstallResult::{Installed, NotFoundInRepo};
|
||||
use crate::package::InstallResult::*;
|
||||
use std::io::Write;
|
||||
|
||||
const OUTDATED_INDEX_CONTENT : &str = include_str!("../res/test/outdated_index.json");
|
||||
|
@ -496,6 +494,36 @@ mod tests {
|
|||
assert_eq!(temp.package_manager.install_package("not-existing").unwrap(), NotFoundInRepo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_install_package_missing_version() {
|
||||
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX);
|
||||
});
|
||||
|
||||
assert_eq!(temp.package_manager.install_package("dummy-package2").unwrap(), MissingPackageVersion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_install_package_missing_readme_unable_to_parse_package_info() {
|
||||
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX);
|
||||
});
|
||||
|
||||
assert_eq!(temp.package_manager.install_package("dummy-package3").unwrap(), UnableToParsePackageInfo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_install_package_bad_readme_unable_to_parse_package_info() {
|
||||
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX);
|
||||
});
|
||||
|
||||
assert_eq!(temp.package_manager.install_package("dummy-package4").unwrap(), UnableToParsePackageInfo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_local_packages() {
|
||||
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||
|
|
BIN
src/res/linux/icon.png
Normal file
BIN
src/res/linux/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -12,6 +12,36 @@
|
|||
|
||||
},
|
||||
|
||||
{
|
||||
"name": "dummy-package2",
|
||||
"title": "Dummy Package",
|
||||
"version": "9.9.9",
|
||||
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||
"desc": "Dummy package",
|
||||
"author": "Federico Terzi"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
"name": "dummy-package3",
|
||||
"title": "Dummy Package",
|
||||
"version": "0.1.0",
|
||||
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||
"desc": "Dummy package",
|
||||
"author": "Federico Terzi"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
"name": "dummy-package4",
|
||||
"title": "Dummy Package",
|
||||
"version": "0.1.0",
|
||||
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||
"desc": "Dummy package",
|
||||
"author": "Federico Terzi"
|
||||
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"name": "italian-accents",
|
||||
|
|
|
@ -19,14 +19,20 @@
|
|||
|
||||
use std::process::Command;
|
||||
use super::MenuItem;
|
||||
use log::error;
|
||||
use log::{error, info};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct LinuxUIManager {}
|
||||
const LINUX_ICON_CONTENT : &[u8] = include_bytes!("../res/linux/icon.png");
|
||||
|
||||
pub struct LinuxUIManager {
|
||||
icon_path: PathBuf,
|
||||
}
|
||||
|
||||
impl super::UIManager for LinuxUIManager {
|
||||
fn notify(&self, message: &str) {
|
||||
let res = Command::new("notify-send")
|
||||
.args(&["-t", "2000", "espanso", message])
|
||||
.args(&["-i", self.icon_path.to_str().unwrap_or_default(),
|
||||
"-t", "2000", "espanso", message])
|
||||
.output();
|
||||
|
||||
if let Err(e) = res {
|
||||
|
@ -45,6 +51,16 @@ impl super::UIManager for LinuxUIManager {
|
|||
|
||||
impl LinuxUIManager {
|
||||
pub fn new() -> LinuxUIManager {
|
||||
LinuxUIManager{}
|
||||
// Initialize the icon if not present
|
||||
let data_dir = crate::context::get_data_dir();
|
||||
let icon_path = data_dir.join("icon.png");
|
||||
if !icon_path.exists() {
|
||||
info!("Creating espanso icon in '{}'", icon_path.to_str().unwrap_or_default());
|
||||
std::fs::write(&icon_path, LINUX_ICON_CONTENT).expect("Unable to copy espanso icon");
|
||||
}
|
||||
|
||||
LinuxUIManager{
|
||||
icon_path
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user