Merge branch 'unstable'

This commit is contained in:
Mike Dilger 2023-05-08 08:07:18 +12:00
commit 190db64dd3
7 changed files with 527 additions and 96 deletions

210
Cargo.lock generated
View File

@ -198,6 +198,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
[[package]]
name = "arboard"
version = "3.2.0"
@ -421,6 +427,26 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
[[package]]
name = "bindgen"
version = "0.64.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
dependencies = [
"bitflags 1.3.2",
"cexpr",
"clang-sys",
"lazy_static",
"lazycell",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 1.0.109",
]
[[package]]
name = "bit_field"
version = "0.10.2"
@ -593,6 +619,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -651,8 +686,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time 0.1.45",
"wasm-bindgen",
"winapi",
]
@ -667,6 +705,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clang-sys"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clipboard-win"
version = "4.5.0"
@ -678,6 +727,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "cmake"
version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
dependencies = [
"cc",
]
[[package]]
name = "cocoa"
version = "0.24.1"
@ -1150,6 +1208,23 @@ dependencies = [
"serde",
]
[[package]]
name = "egui-video"
version = "0.1.0"
source = "git+https://github.com/n00kii/egui-video?rev=50b4efd6d8250b555a85f966deca737c161ee6df#50b4efd6d8250b555a85f966deca737c161ee6df"
dependencies = [
"anyhow",
"chrono",
"egui",
"ffmpeg-next",
"itertools",
"parking_lot",
"ringbuf",
"sdl2",
"tempfile",
"timer",
]
[[package]]
name = "egui-winit"
version = "0.21.1"
@ -1380,6 +1455,30 @@ dependencies = [
"subtle",
]
[[package]]
name = "ffmpeg-next"
version = "6.0.0"
source = "git+https://github.com/bu5hm4nn/rust-ffmpeg?rev=a08d9f86c2d2fb05fcf6ceb65ae68ef788964f18#a08d9f86c2d2fb05fcf6ceb65ae68ef788964f18"
dependencies = [
"bitflags 1.3.2",
"ffmpeg-sys-next",
"libc",
]
[[package]]
name = "ffmpeg-sys-next"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf650f461ccf130f4eef4927affed703cc387b183bfc4a7dfee86a076c131127"
dependencies = [
"bindgen",
"cc",
"libc",
"num_cpus",
"pkg-config",
"vcpkg",
]
[[package]]
name = "flate2"
version = "1.0.25"
@ -1573,7 +1672,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
@ -1598,6 +1697,12 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "glow"
version = "0.12.1"
@ -1684,6 +1789,7 @@ dependencies = [
"dashmap",
"dirs 5.0.0",
"eframe",
"egui-video",
"egui-winit",
"egui_extras",
"encoding_rs",
@ -1706,15 +1812,17 @@ dependencies = [
"regex",
"reqwest",
"rusqlite",
"sdl2",
"serde",
"serde_json",
"sha2",
"time",
"time 0.3.20",
"tokio",
"tokio-tungstenite",
"tracing",
"tracing-subscriber",
"tungstenite",
"url",
"vecmap-rs",
"zeroize",
]
@ -2029,6 +2137,15 @@ version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.6"
@ -2119,6 +2236,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lebe"
version = "0.5.2"
@ -2332,7 +2455,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.45.0",
]
@ -2755,6 +2878,12 @@ dependencies = [
"sha2",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "percent-encoding"
version = "2.2.0"
@ -3124,6 +3253,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "ringbuf"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "ron"
version = "0.8.0"
@ -3160,6 +3298,12 @@ dependencies = [
"smallvec",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -3290,6 +3434,28 @@ dependencies = [
"tiny-skia",
]
[[package]]
name = "sdl2"
version = "0.35.2"
source = "git+https://github.com/Rust-SDL2/rust-sdl2?rev=27cd1fd67c811e06b9d997a77bb6089a1b65070d#27cd1fd67c811e06b9d997a77bb6089a1b65070d"
dependencies = [
"bitflags 1.3.2",
"lazy_static",
"libc",
"sdl2-sys",
]
[[package]]
name = "sdl2-sys"
version = "0.35.2"
source = "git+https://github.com/Rust-SDL2/rust-sdl2?rev=27cd1fd67c811e06b9d997a77bb6089a1b65070d#27cd1fd67c811e06b9d997a77bb6089a1b65070d"
dependencies = [
"cfg-if",
"cmake",
"libc",
"version-compare",
]
[[package]]
name = "sec1"
version = "0.7.2"
@ -3418,6 +3584,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@ -3667,6 +3839,17 @@ dependencies = [
"weezl",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "time"
version = "0.3.20"
@ -3694,6 +3877,15 @@ dependencies = [
"time-core",
]
[[package]]
name = "timer"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b"
dependencies = [
"chrono",
]
[[package]]
name = "tiny-skia"
version = "0.8.4"
@ -4056,6 +4248,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfd18cb0279ed472822644fed8a86507fa8578241d4e047eddb3129591350cc0"
[[package]]
name = "version-compare"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
[[package]]
name = "version_check"
version = "0.9.4"
@ -4088,6 +4286,12 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

View File

@ -12,10 +12,22 @@ edition = "2021"
[features]
default = ["rustls-tls"]
lang-cjk = []
video-ffmpeg = [ "egui-video", "sdl2" ]
native-tls = [ "reqwest/native-tls", "tungstenite/native-tls", "tokio-tungstenite/native-tls"]
rustls-tls = [ "reqwest/rustls-tls", "tungstenite/rustls-tls-webpki-roots", "tokio-tungstenite/rustls-tls-webpki-roots"]
[patch.crates-io]
# override egui crate for egui_video dependency
egui = { git = "https://github.com/mikedilger/egui", rev = "a18ee5e5867afb7c80c8cd0bc2815278274124b3" }
# Use the master branch of SDL2 to include a fix related to clang (and XCode after 14.2)
sdl2 = { git = "https://github.com/Rust-SDL2/rust-sdl2", rev = "27cd1fd67c811e06b9d997a77bb6089a1b65070d" }
[patch.'https://github.com/n00kii/rust-ffmpeg']
# Use upstream which is newer
ffmpeg-next = { git = "https://github.com/bu5hm4nn/rust-ffmpeg", rev = "a08d9f86c2d2fb05fcf6ceb65ae68ef788964f18" }
[dependencies]
async-recursion = "1.0"
async-trait = "0.1"
@ -25,6 +37,7 @@ dirs = "5.0"
eframe = { git = "https://github.com/mikedilger/egui", rev = "a18ee5e5867afb7c80c8cd0bc2815278274124b3", features = [ "persistence" ] }
egui-winit = { git = "https://github.com/mikedilger/egui", rev = "a18ee5e5867afb7c80c8cd0bc2815278274124b3", features = [ "default" ] }
egui_extras = { git = "https://github.com/mikedilger/egui", rev = "a18ee5e5867afb7c80c8cd0bc2815278274124b3", features = [ "image", "svg" ] }
egui-video = { git = "https://github.com/n00kii/egui-video", rev = "50b4efd6d8250b555a85f966deca737c161ee6df", features = [ "from_bytes" ], optional = true }
encoding_rs = "0.8"
fallible-iterator = "0.2"
futures = "0.3"
@ -45,6 +58,7 @@ rand = "0.8"
regex = "1.8"
reqwest = { version = "0.11", default-features=false, features = ["brotli", "deflate", "gzip", "json"] }
rusqlite = { version = "0.29", features = ["bundled", "chrono", "serde_json"] }
sdl2 = { version = "0.35.2", features = ["bundled"], optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
@ -54,6 +68,7 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = [ "std", "env-filter" ] }
tokio-tungstenite = { version = "0.18", default-features = false, features = [ "connect", "handshake" ] }
tungstenite = { version = "0.18", default-features = false }
url = "2.3"
vecmap-rs = "0.1"
zeroize = "1.6"

View File

@ -117,6 +117,16 @@ Most dependencies are probably already installed in your base operating system.
- openssl (debian: "libssl-dev")
- fontconfig (debian: "libfontconfig1-dev")
#### macOS
a. Install rust with rust-up: https://rustup.rs/
b. Install homebrew if you don't have it yet https://brew.sh/
c. Install these dependencies:
```
brew install cmake sdl2 pkg-config ffmpeg
```
### Step 3 - Clone this Repository
````bash
@ -189,15 +199,25 @@ Gossip by default does not include the CJK font because it is larger than all ot
There are so many of these (172) that it becomes a real pain to add them all. But if you need one, please ask (open an issue) and I'll add it for you.
### Known Issues
### Video Playback
#### Sqlite Constraint Issues (Foreign or Unique Key)
You will need to install sdl2 (follow the instructions in the [readme](https://github.com/Rust-SDL2/rust-sdl2/)) and ffmpeg on your system.
Compile with
````
--features=video-ffmpeg
````
## Known Issues
### Sqlite Constraint Issues (Foreign or Unique Key)
First you need to locate your database file. The gossip directory is under this path: https://docs.rs/dirs/4.0.0/dirs/fn.data_dir.html The database file is `gossip.sqlite`. Then you need to install `sqlite3` on your system.
Using `sqlite3` on your database file, the following kind of SQL can help you identify rows that violate foreign key constraints.
##### Error: Sql(SqliteFailure(Error { code: ConstraintViolation, extended_code: 2067 }, Some("UNIQUE constraint failed: person_relay.person, person_relay.relay")))
#### Error: Sql(SqliteFailure(Error { code: ConstraintViolation, extended_code: 2067 }, Some("UNIQUE constraint failed: person_relay.person, person_relay.relay")))
You can find which rows are duplicated using: `select a.person, a.relay FROM person_relay a INNER JOIN person_relay b WHERE a.person=b.person AND a.relay=b.relay AND a.rowid!=b.rowid;` You'll need to delete one row from each pair (by rowid so you don't delete both of them).

2
run.sh
View File

@ -1,5 +1,5 @@
#!/bin/bash
RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable" cargo build --release && \
RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable" cargo build --features=lang-cjk,video-ffmpeg --release && \
RUST_BACKTRACE=1 RUST_LOG="info,gossip=debug" ./target/release/gossip

View File

@ -12,7 +12,8 @@ pub struct Media {
// until the UI next asks for them, at which point we remove them
// and hand them over. This way we can do the work that takes
// longer and the UI can do as little work as possible.
media_temp: DashMap<Url, ColorImage>,
image_temp: DashMap<Url, ColorImage>,
data_temp: DashMap<Url, Vec<u8>>,
media_pending_processing: DashSet<Url>,
failed_media: RwLock<HashSet<UncheckedUrl>>,
}
@ -20,7 +21,8 @@ pub struct Media {
impl Media {
pub fn new() -> Media {
Media {
media_temp: DashMap::new(),
image_temp: DashMap::new(),
data_temp: DashMap::new(),
media_pending_processing: DashSet::new(),
failed_media: RwLock::new(HashSet::new()),
}
@ -33,7 +35,6 @@ impl Media {
Err(_) => {
// this cannot recover without new metadata
self.failed_media.blocking_write().insert(unchecked_url);
return None;
}
};
@ -48,18 +49,9 @@ impl Media {
self.failed_media.blocking_write().remove(unchecked_url);
}
pub fn get_media(&self, url: &Url) -> Option<ColorImage> {
// If it failed before, error out now
if self
.failed_media
.blocking_read()
.contains(&url.to_unchecked_url())
{
return None; // cannot recover.
}
pub fn get_image(&self, url: &Url) -> Option<ColorImage> {
// If we have it, hand it over (we won't need a copy anymore)
if let Some(th) = self.media_temp.remove(url) {
if let Some(th) = self.image_temp.remove(url) {
return Some(th.1);
}
@ -68,14 +60,8 @@ impl Media {
return None; // will recover after processing completes
}
// Do not fetch if disabled
if !GLOBALS.settings.read().load_media {
return None; // can recover if the setting is switched
}
match GLOBALS.fetcher.try_get(url.clone()) {
Ok(None) => None,
Ok(Some(bytes)) => {
match self.get_data(url) {
Some(bytes) => {
// Finish this later (spawn)
let aurl = url.to_owned();
tokio::spawn(async move {
@ -85,12 +71,12 @@ impl Media {
.load(Ordering::Relaxed)
/ 100;
if let Ok(color_image) = egui_extras::image::load_image_bytes(&bytes) {
GLOBALS.media.media_temp.insert(aurl, color_image);
GLOBALS.media.image_temp.insert(aurl, color_image);
} else if let Ok(color_image) = egui_extras::image::load_svg_bytes_with_size(
&bytes,
FitTo::Size(size, size),
) {
GLOBALS.media.media_temp.insert(aurl, color_image);
GLOBALS.media.image_temp.insert(aurl, color_image);
} else {
// this cannot recover without new metadata
GLOBALS
@ -104,6 +90,36 @@ impl Media {
self.media_pending_processing.insert(url.clone());
None
}
None => None,
}
}
pub fn get_data(&self, url: &Url) -> Option<Vec<u8>> {
// If it failed before, error out now
if self
.failed_media
.blocking_read()
.contains(&url.to_unchecked_url())
{
return None; // cannot recover.
}
// If we have it, hand it over (we won't need a copy anymore)
if let Some(th) = self.data_temp.remove(url) {
return Some(th.1);
}
// Do not fetch if disabled
if !GLOBALS.settings.read().load_media {
return None; // can recover if the setting is switched
}
match GLOBALS.fetcher.try_get(url.clone()) {
Ok(None) => None,
Ok(Some(bytes)) => {
self.data_temp.insert(url.clone(), bytes);
None
}
Err(e) => {
tracing::error!("{}", e);
// this cannot recover without new metadata

View File

@ -115,11 +115,14 @@ pub(super) fn render_hyperlink(
linkspan: &Span,
) {
let link = note.shattered_content.slice(linkspan).unwrap();
if let Some(image_url) = as_image_url(app, link) {
show_image_toggle(app, ui, image_url);
//} else if is_video_url(&lowercase) {
// TODO
// crate::ui::widgets::break_anywhere_hyperlink_to(ui, link, link);
if let (Ok(url), Some(nurl)) = (url::Url::try_from(link), app.try_check_url(link)) {
if is_image_url(&url) {
show_image_toggle(app, ui, nurl);
} else if is_video_url(&url) {
show_video_toggle(app, ui, nurl);
} else {
crate::ui::widgets::break_anywhere_hyperlink_to(ui, link, link);
}
} else {
crate::ui::widgets::break_anywhere_hyperlink_to(ui, link, link);
}
@ -172,8 +175,8 @@ pub(super) fn render_unknown_reference(ui: &mut Ui, num: usize) {
}
}
fn is_image_url(url: &str) -> bool {
let lower = url.to_lowercase();
fn is_image_url(url: &url::Url) -> bool {
let lower = url.path().to_lowercase();
lower.ends_with(".jpg")
|| lower.ends_with(".jpeg")
|| lower.ends_with(".png")
@ -181,23 +184,13 @@ fn is_image_url(url: &str) -> bool {
|| lower.ends_with(".webp")
}
fn as_image_url(app: &mut GossipUi, url: &str) -> Option<Url> {
if is_image_url(url) {
app.try_check_url(url)
} else {
None
}
}
/*
fn is_video_url(url: &str) -> bool {
let lower = url.to_lowercase();
fn is_video_url(url: &url::Url) -> bool {
let lower = url.path().to_lowercase();
lower.ends_with(".mov")
|| lower.ends_with(".mp4")
|| lower.ends_with(".mkv")
|| lower.ends_with(".webm")
}
*/
fn show_image_toggle(app: &mut GossipUi, ui: &mut Ui, url: Url) {
let row_height = ui.cursor().height();
@ -209,7 +202,7 @@ fn show_image_toggle(app: &mut GossipUi, ui: &mut Ui, url: Url) {
|| (!app.settings.show_media && app.media_show_list.contains(&url));
if show_image {
if let Some(response) = try_render_media(app, ui, url.clone()) {
if let Some(response) = try_render_image(app, ui, url.clone()) {
show_link = false;
// full-width toggle
@ -268,54 +261,20 @@ fn show_image_toggle(app: &mut GossipUi, ui: &mut Ui, url: Url) {
/// Try to fetch and render a piece of media
/// - return: true if successfully rendered, false otherwise
fn try_render_media(app: &mut GossipUi, ui: &mut Ui, url: Url) -> Option<Response> {
fn try_render_image(app: &mut GossipUi, ui: &mut Ui, url: Url) -> Option<Response> {
let mut response_return = None;
if let Some(media) = app.try_get_media(ui.ctx(), url.clone()) {
let ui_max = if app.media_full_width_list.contains(&url) {
Vec2::new(
ui.available_width() * 0.9,
ui.ctx().screen_rect().height() * 0.9,
)
} else {
Vec2::new(
ui.available_width() / 2.0,
ui.ctx().screen_rect().height() / 3.0,
)
};
let msize = media.size_vec2();
let aspect = media.aspect_ratio();
let size = media_scale(
app.media_full_width_list.contains(&url),
ui,
media.size_vec2(),
);
// insert a newline if the current line has text
if ui.cursor().min.x > ui.max_rect().min.x {
ui.end_row();
}
// determine maximum x and y sizes
let max_x = if ui_max.x > msize.x {
msize.x
} else {
ui_max.x
};
let max_y = if ui_max.y > msize.y {
msize.y
} else {
ui_max.y
};
// now determine if we are constrained by x or by y and
// calculate the resulting size
let mut size = Vec2::new(0.0, 0.0);
size.x = if max_x > max_y * aspect {
max_y * aspect
} else {
max_x
};
size.y = if max_y > max_x / aspect {
max_x / aspect
} else {
max_y
};
// render the image with a nice frame around it
egui::Frame::none()
.inner_margin(egui::Margin::same(0.0))
@ -396,3 +355,151 @@ fn try_render_media(app: &mut GossipUi, ui: &mut Ui, url: Url) -> Option<Respons
};
response_return
}
fn show_video_toggle(app: &mut GossipUi, ui: &mut Ui, url: Url) {
let row_height = ui.cursor().height();
let url_string = url.to_string();
let mut show_link = true;
// FIXME show/hide lists should persist app restarts
let show_video = (app.settings.show_media && !app.media_hide_list.contains(&url))
|| (!app.settings.show_media && app.media_show_list.contains(&url));
if show_video {
if let Some(response) = try_render_video(app, ui, url.clone()) {
show_link = false;
// full-width toggle
if response.clicked() {
if app.media_full_width_list.contains(&url) {
app.media_full_width_list.remove(&url);
} else {
app.media_full_width_list.insert(url.clone());
}
}
}
}
if show_link {
let response = ui.link("[ Video ]");
// show url on hover
response.clone().on_hover_text(url_string.clone());
// show media toggle
if response.clicked() {
if app.settings.show_media {
app.media_hide_list.remove(&url);
} else {
app.media_show_list.insert(url.clone());
}
if !app.settings.load_media {
*GLOBALS.status_message.blocking_write() = "Fetch Media setting is disabled. Right-click link to open in browser or copy URL".to_owned();
}
}
// context menu
response.context_menu(|ui| {
if ui.button("Open in browser").clicked() {
let modifiers = ui.ctx().input(|i| i.modifiers);
ui.ctx().output_mut(|o| {
o.open_url = Some(egui::output::OpenUrl {
url: url_string.clone(),
new_tab: modifiers.any(),
});
});
}
if ui.button("Copy URL").clicked() {
ui.output_mut(|o| o.copied_text = url_string.clone());
}
if app.has_media_loading_failed(url_string.as_str())
&& ui.button("Retry loading ...").clicked()
{
app.retry_media(&url);
}
});
}
ui.end_row();
// workaround for egui bug where image enlarges the cursor height
ui.set_row_height(row_height);
}
#[cfg(feature = "video-ffmpeg")]
fn try_render_video(app: &mut GossipUi, ui: &mut Ui, url: Url) -> Option<Response> {
let mut response_return = None;
let show_full_width = app.media_full_width_list.contains(&url);
if let Some(player_ref) = app.try_get_player(ui.ctx(), url) {
if let Ok(mut player) = player_ref.try_borrow_mut() {
let size = media_scale(
show_full_width,
ui,
Vec2 {
x: player.width as f32,
y: player.height as f32,
},
);
// insert a newline if the current line has text
if ui.cursor().min.x > ui.max_rect().min.x {
ui.end_row();
}
// show the player
if !show_full_width {
player.stop();
}
let response = player.ui(ui, [size.x, size.y]);
// TODO fix click action
let new_rect = response.rect.shrink(size.x / 2.0);
response_return = Some(response.with_new_rect(new_rect))
}
}
response_return
}
#[cfg(not(feature = "video-ffmpeg"))]
fn try_render_video(_app: &mut GossipUi, _ui: &mut Ui, _url: Url) -> Option<Response> {
return None;
}
fn media_scale(show_full_width: bool, ui: &Ui, media_size: Vec2) -> Vec2 {
let aspect = media_size.x / media_size.y;
let ui_max = if show_full_width {
Vec2::new(
ui.available_width() * 0.9,
ui.ctx().screen_rect().height() * 0.9,
)
} else {
Vec2::new(
ui.available_width() / 2.0,
ui.ctx().screen_rect().height() / 3.0,
)
};
// determine maximum x and y sizes
let max_x = if ui_max.x > media_size.x {
media_size.x
} else {
ui_max.x
};
let max_y = if ui_max.y > media_size.y {
media_size.y
} else {
ui_max.y
};
// now determine if we are constrained by x or by y and
// calculate the resulting size
let mut size = Vec2::new(0.0, 0.0);
size.x = if max_x > max_y * aspect {
max_y * aspect
} else {
max_x
};
size.y = if max_y > max_x / aspect {
max_x / aspect
} else {
max_y
};
size
}

View File

@ -32,13 +32,19 @@ use crate::people::DbPerson;
use crate::settings::Settings;
pub use crate::ui::theme::{Theme, ThemeVariant};
use crate::ui::widgets::CopyButton;
#[cfg(feature = "video-ffmpeg")]
use core::cell::RefCell;
use eframe::{egui, IconData};
use egui::{
Color32, ColorImage, Context, Image, ImageData, Label, RichText, SelectableLabel, Sense,
TextStyle, TextureHandle, TextureOptions, Ui, Vec2,
};
#[cfg(feature = "video-ffmpeg")]
use egui_video::{AudioDevice, Player};
use nostr_types::{Id, IdHex, Metadata, PublicKey, PublicKeyHex, RelayUrl, UncheckedUrl, Url};
use std::collections::{HashMap, HashSet};
#[cfg(feature = "video-ffmpeg")]
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::time::{Duration, Instant};
use zeroize::Zeroize;
@ -108,6 +114,11 @@ pub enum HighlightType {
}
struct GossipUi {
#[cfg(feature = "video-ffmpeg")]
audio_device: Option<AudioDevice>,
#[cfg(feature = "video-ffmpeg")]
video_players: HashMap<Url, Rc<RefCell<egui_video::Player>>>,
// Rendering
next_frame: Instant,
override_dpi: bool,
@ -144,7 +155,7 @@ struct GossipUi {
placeholder_avatar: TextureHandle,
settings: Settings,
avatars: HashMap<PublicKeyHex, TextureHandle>,
media: HashMap<Url, TextureHandle>,
images: HashMap<Url, TextureHandle>,
/// used when settings.show_media=false to explicitly show
media_show_list: HashSet<Url>,
/// used when settings.show_media=false to explicitly hide
@ -261,6 +272,19 @@ impl GossipUi {
)
};
#[cfg(feature = "video-ffmpeg")]
let audio_device = {
let mut device = None;
if let Ok(init) = sdl2::init() {
if let Ok(audio) = init.audio() {
if let Ok(dev) = egui_video::init_audio_device(&audio) {
device = Some(dev);
}
}
}
device
};
// how to load an svg
// let expand_right_symbol = {
// let bytes = include_bytes!("../../assets/expand-image.svg");
@ -293,6 +317,10 @@ impl GossipUi {
theme::apply_theme(settings.theme, &cctx.egui_ctx);
GossipUi {
#[cfg(feature = "video-ffmpeg")]
audio_device,
#[cfg(feature = "video-ffmpeg")]
video_players: HashMap::new(),
next_frame: Instant::now(),
override_dpi,
override_dpi_value,
@ -315,7 +343,7 @@ impl GossipUi {
placeholder_avatar: placeholder_avatar_texture_handle,
settings,
avatars: HashMap::new(),
media: HashMap::new(),
images: HashMap::new(),
media_show_list: HashSet::new(),
media_hide_list: HashSet::new(),
media_full_width_list: HashSet::new(),
@ -762,20 +790,61 @@ impl GossipUi {
}
// see if we already have a texturehandle for this media
if let Some(th) = self.media.get(&url) {
if let Some(th) = self.images.get(&url) {
return Some(th.to_owned());
}
if let Some(color_image) = GLOBALS.media.get_media(&url) {
if let Some(color_image) = GLOBALS.media.get_image(&url) {
let texture_handle =
ctx.load_texture(url.0.clone(), color_image, TextureOptions::default());
self.media.insert(url, texture_handle.clone());
self.images.insert(url, texture_handle.clone());
Some(texture_handle)
} else {
None
}
}
#[cfg(feature = "video-ffmpeg")]
pub fn try_get_player(
&mut self,
ctx: &Context,
url: Url,
) -> Option<Rc<RefCell<egui_video::Player>>> {
// Do not keep retrying if failed
if GLOBALS.media.has_failed(&url.to_unchecked_url()) {
return None;
}
// see if we already have a player for this video
if let Some(player) = self.video_players.get(&url) {
return Some(player.to_owned());
}
if let Some(bytes) = GLOBALS.media.get_data(&url) {
if let Ok(player) = Player::new_from_bytes(ctx, &bytes) {
if let Some(audio) = &mut self.audio_device {
if let Ok(player) = player.with_audio(audio) {
let player_ref = Rc::new(RefCell::new(player));
self.video_players.insert(url.clone(), player_ref.clone());
Some(player_ref)
} else {
GLOBALS.media.has_failed(&url.to_unchecked_url());
None
}
} else {
let player_ref = Rc::new(RefCell::new(player));
self.video_players.insert(url.clone(), player_ref.clone());
Some(player_ref)
}
} else {
GLOBALS.media.has_failed(&url.to_unchecked_url());
None
}
} else {
None
}
}
pub fn render_qr(&mut self, ui: &mut Ui, ctx: &Context, key: &str, content: &str) {
// Remember the UI runs this every frame. We do NOT want to load the texture to the GPU
// every frame, so we remember the texture handle in app.qr_codes, and only load to the GPU