Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e4f2f2127 | ||
|
|
b6eac330ca | ||
|
|
7fb1c96ca9 | ||
|
|
80a5b73e39 |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 606 B After Width: | Height: | Size: 843 B |
|
Before Width: | Height: | Size: 994 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -1,41 +1,66 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- PeerSpeak app icon: in-app mic glyph + P2P mesh nodes, Catppuccin Mocha. -->
|
<title>PeerSpeak</title>
|
||||||
|
<desc>Two luminous voices meet directly to form a flowing S.</desc>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="tile" x1="32" y1="20" x2="225" y2="239" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0" stop-color="#1e1e2e"/>
|
<stop stop-color="#101d42"/>
|
||||||
<stop offset="1" stop-color="#181825"/>
|
<stop offset="0.5" stop-color="#071225"/>
|
||||||
|
<stop offset="1" stop-color="#160b31"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<linearGradient id="voice" x1="45" y1="76" x2="214" y2="184" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#35efff"/>
|
||||||
|
<stop offset="0.42" stop-color="#2583ff"/>
|
||||||
|
<stop offset="0.68" stop-color="#8a42ff"/>
|
||||||
|
<stop offset="1" stop-color="#ff3cdd"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="edge" x1="30" y1="31" x2="225" y2="231" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#2fe9ff" stop-opacity="0.7"/>
|
||||||
|
<stop offset="0.48" stop-color="#386dff" stop-opacity="0.18"/>
|
||||||
|
<stop offset="1" stop-color="#eb42ff" stop-opacity="0.65"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="core">
|
||||||
|
<stop stop-color="#ffffff"/>
|
||||||
|
<stop offset="0.28" stop-color="#baf7ff"/>
|
||||||
|
<stop offset="0.62" stop-color="#7b67ff" stop-opacity="0.65"/>
|
||||||
|
<stop offset="1" stop-color="#7b67ff" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<filter id="shadow" x="-35%" y="-35%" width="170%" height="170%">
|
||||||
|
<feGaussianBlur stdDeviation="6"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="soft-shadow" x="-20%" y="-20%" width="140%" height="150%">
|
||||||
|
<feDropShadow dx="0" dy="7" stdDeviation="7" flood-color="#000611" flood-opacity="0.8"/>
|
||||||
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<!-- Rounded-square tile -->
|
<!-- A dark stage makes the cyan/violet conversation mark legible at taskbar size. -->
|
||||||
<rect x="20" y="20" width="216" height="216" rx="48" fill="url(#tile)"
|
<rect x="8" y="8" width="240" height="240" rx="55" fill="url(#tile)"/>
|
||||||
stroke="#313244" stroke-width="3"/>
|
<rect x="9.5" y="9.5" width="237" height="237" rx="53.5" fill="none" stroke="url(#edge)" stroke-width="3"/>
|
||||||
|
|
||||||
<!-- P2P mesh: edges (under nodes + mic) -->
|
<!-- Broad color glow, kept behind the silhouette. -->
|
||||||
<g stroke="#45475a" stroke-width="6" stroke-linecap="round" fill="none">
|
<path d="M36 128 C50 128 48 101 60 101 C72 101 68 153 81 153 C94 153 90 112 104 112 C115 112 116 128 128 128 C140 128 141 144 152 144 C166 144 162 103 175 103 C188 103 184 155 196 155 C208 155 206 128 220 128"
|
||||||
<line x1="74" y1="74" x2="128" y2="128"/>
|
fill="none" stroke="url(#voice)" stroke-width="25" stroke-linecap="round"
|
||||||
<line x1="182" y1="74" x2="128" y2="128"/>
|
opacity="0.5" filter="url(#shadow)"/>
|
||||||
<line x1="74" y1="182" x2="128" y2="128"/>
|
|
||||||
<line x1="182" y1="182" x2="128" y2="128"/>
|
|
||||||
<line x1="74" y1="74" x2="182" y2="74"/>
|
|
||||||
<line x1="74" y1="182" x2="182" y2="182"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- P2P mesh: peer nodes -->
|
<!-- The two waveform halves are equal peers and meet at one bright point. -->
|
||||||
<g fill="#b4befe">
|
<path d="M36 128 C50 128 48 101 60 101 C72 101 68 153 81 153 C94 153 90 112 104 112 C115 112 116 128 128 128"
|
||||||
<circle cx="74" cy="74" r="11"/>
|
fill="none" stroke="url(#voice)" stroke-width="17" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<circle cx="182" cy="74" r="11"/>
|
<path d="M128 128 C140 128 141 144 152 144 C166 144 162 103 175 103 C188 103 184 155 196 155 C208 155 206 128 220 128"
|
||||||
<circle cx="74" cy="182" r="11"/>
|
fill="none" stroke="url(#voice)" stroke-width="17" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<circle cx="182" cy="182" r="11"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Microphone (hero) — same geometry as the in-app Mic icon, scaled 6.4x -->
|
<!-- A single flowing connection turns the conversation into PeerSpeak's S-mark. -->
|
||||||
<g fill="none" stroke="#89b4fa" stroke-width="13"
|
<path d="M160 66 C142 52 108 57 101 78 C94 98 113 111 128 119 C148 130 163 141 157 164 C151 188 117 199 94 184"
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
fill="none" stroke="#050b1b" stroke-opacity="0.72" stroke-width="33"
|
||||||
<rect x="108.8" y="68.8" width="38.4" height="70.4" rx="19.2"/>
|
stroke-linecap="round" stroke-linejoin="round" filter="url(#soft-shadow)"/>
|
||||||
<path d="M 169.6 123.2 A 41.6 41.6 0 0 0 86.4 123.2"/>
|
<path d="M160 66 C142 52 108 57 101 78 C94 98 113 111 128 119 C148 130 163 141 157 164 C151 188 117 199 94 184"
|
||||||
<line x1="128" y1="164.8" x2="128" y2="187.2"/>
|
fill="none" stroke="url(#voice)" stroke-width="25"
|
||||||
<line x1="105.6" y1="187.2" x2="150.4" y2="187.2"/>
|
stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</g>
|
<path d="M157 65 C139 56 113 62 108 79" fill="none" stroke="#bdf9ff"
|
||||||
|
stroke-opacity="0.68" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M153 166 C146 184 118 192 98 181" fill="none" stroke="#f4a8ff"
|
||||||
|
stroke-opacity="0.52" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- The direct connection is the brightest and simplest detail. -->
|
||||||
|
<circle cx="128" cy="128" r="30" fill="url(#core)" opacity="0.78" filter="url(#shadow)"/>
|
||||||
|
<circle cx="128" cy="128" r="6.5" fill="#ffffff"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: mollusk <jitty+lc1iz0dc@protonmail.com>
|
# Maintainer: mollusk <jitty+lc1iz0dc@protonmail.com>
|
||||||
pkgname=peerspeak-git
|
pkgname=peerspeak-git
|
||||||
_pkgname=peerspeak
|
_pkgname=peerspeak
|
||||||
pkgver=0.2.0.r218.gcbba4b6
|
pkgver=0.3.0.r229.g7fb1c96
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Decentralized peer-to-peer voice chat (Rust/iroh/PipeWire/Opus/iced)"
|
pkgdesc="Decentralized peer-to-peer voice chat (Rust/iroh/PipeWire/Opus/iced)"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 364 KiB |
@@ -287,6 +287,8 @@ pub enum AppMessage {
|
|||||||
AttachmentFilePicked(Option<(String, Vec<u8>)>),
|
AttachmentFilePicked(Option<(String, Vec<u8>)>),
|
||||||
/// Save (downloading first if needed) a received attachment to disk.
|
/// Save (downloading first if needed) a received attachment to disk.
|
||||||
SaveAttachment(crate::files::AttachmentId),
|
SaveAttachment(crate::files::AttachmentId),
|
||||||
|
/// Result of the async save dialog: a status line to show, or None if cancelled.
|
||||||
|
AttachmentSaved(Option<String>),
|
||||||
/// Fetch (if needed) and start an inline audio attachment.
|
/// Fetch (if needed) and start an inline audio attachment.
|
||||||
PlayAudio(crate::files::AttachmentId),
|
PlayAudio(crate::files::AttachmentId),
|
||||||
PauseAudio,
|
PauseAudio,
|
||||||
@@ -1094,12 +1096,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
let needs_save = state.pending_saves.remove(&id);
|
let needs_save = state.pending_saves.remove(&id);
|
||||||
let needs_play = state.pending_plays.remove(&id);
|
let needs_play = state.pending_plays.remove(&id);
|
||||||
state.attachment_data.insert(id, AttachmentState::Ready(data));
|
state.attachment_data.insert(id, AttachmentState::Ready(data));
|
||||||
if needs_save {
|
|
||||||
save_attachment_to_disk(state, id);
|
|
||||||
}
|
|
||||||
if needs_play {
|
if needs_play {
|
||||||
play_ready_audio(state, id);
|
play_ready_audio(state, id);
|
||||||
}
|
}
|
||||||
|
if needs_save {
|
||||||
|
return save_attachment_task(state, id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UiEvent::AttachmentFailed { id, error } => {
|
UiEvent::AttachmentFailed { id, error } => {
|
||||||
state.pending_saves.remove(&id);
|
state.pending_saves.remove(&id);
|
||||||
@@ -1663,7 +1665,7 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
// If we already have the bytes, save now; otherwise fetch from the
|
// If we already have the bytes, save now; otherwise fetch from the
|
||||||
// sender and save when AttachmentReady arrives (pending_saves).
|
// sender and save when AttachmentReady arrives (pending_saves).
|
||||||
if matches!(state.attachment_data.get(&id), Some(AttachmentState::Ready(_))) {
|
if matches!(state.attachment_data.get(&id), Some(AttachmentState::Ready(_))) {
|
||||||
save_attachment_to_disk(state, id);
|
return save_attachment_task(state, id);
|
||||||
} else if let Some((from, att)) = find_attachment_source(state, id) {
|
} else if let Some((from, att)) = find_attachment_source(state, id) {
|
||||||
if let Ok(eid) = from.parse::<EndpointId>() {
|
if let Ok(eid) = from.parse::<EndpointId>() {
|
||||||
state.pending_saves.insert(id);
|
state.pending_saves.insert(id);
|
||||||
@@ -1676,6 +1678,11 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppMessage::AttachmentSaved(msg) => {
|
||||||
|
if let Some(m) = msg {
|
||||||
|
state.status_message = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
AppMessage::PlayAudio(id) => {
|
AppMessage::PlayAudio(id) => {
|
||||||
if matches!(state.attachment_data.get(&id), Some(AttachmentState::Ready(_))) {
|
if matches!(state.attachment_data.get(&id), Some(AttachmentState::Ready(_))) {
|
||||||
play_ready_audio(state, id);
|
play_ready_audio(state, id);
|
||||||
@@ -1982,11 +1989,18 @@ fn play_ready_audio(state: &mut AppState, id: crate::files::AttachmentId) {
|
|||||||
|
|
||||||
/// Write a ready attachment's bytes to a user-chosen location via a native save
|
/// Write a ready attachment's bytes to a user-chosen location via a native save
|
||||||
/// dialog. The default filename comes from the (already-sanitized) descriptor.
|
/// dialog. The default filename comes from the (already-sanitized) descriptor.
|
||||||
/// No-op if the bytes aren't ready. Sync dialog: the brief block is acceptable
|
/// Build a Task that opens the native save dialog off the UI thread and writes
|
||||||
/// for a deliberate save action.
|
/// the (already-fetched) attachment bytes to the chosen path. No-op task if the
|
||||||
fn save_attachment_to_disk(state: &mut AppState, id: crate::files::AttachmentId) {
|
/// bytes aren't ready.
|
||||||
|
///
|
||||||
|
/// MUST be async (`rfd::AsyncFileDialog`): the blocking `rfd::FileDialog` spins
|
||||||
|
/// its own GTK loop, and invoking it from inside iced's running event loop with
|
||||||
|
/// the Linux `xdg-desktop-portal`/GTK backend wedges the dialog — Save/Cancel
|
||||||
|
/// stop responding. The file *picker* paths already use the async variant; this
|
||||||
|
/// is the one save path that must match.
|
||||||
|
fn save_attachment_task(state: &AppState, id: crate::files::AttachmentId) -> Task<AppMessage> {
|
||||||
let Some(AttachmentState::Ready(data)) = state.attachment_data.get(&id) else {
|
let Some(AttachmentState::Ready(data)) = state.attachment_data.get(&id) else {
|
||||||
return;
|
return Task::none();
|
||||||
};
|
};
|
||||||
let data = data.clone();
|
let data = data.clone();
|
||||||
let default_name = state
|
let default_name = state
|
||||||
@@ -1999,16 +2013,23 @@ fn save_attachment_to_disk(state: &mut AppState, id: crate::files::AttachmentId)
|
|||||||
.map(|a| a.name.clone())
|
.map(|a| a.name.clone())
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "download".to_string());
|
.unwrap_or_else(|| "download".to_string());
|
||||||
if let Some(path) = rfd::FileDialog::new()
|
Task::perform(
|
||||||
.set_file_name(default_name)
|
async move {
|
||||||
.set_title("Save attachment")
|
let handle = rfd::AsyncFileDialog::new()
|
||||||
.save_file()
|
.set_file_name(default_name)
|
||||||
{
|
.set_title("Save attachment")
|
||||||
match std::fs::write(&path, &data) {
|
.save_file()
|
||||||
Ok(()) => state.status_message = format!("Saved {}", path.display()),
|
.await;
|
||||||
Err(e) => state.status_message = format!("Save failed: {e}"),
|
match handle {
|
||||||
}
|
Some(h) => match std::fs::write(h.path(), &data) {
|
||||||
}
|
Ok(()) => Some(format!("Saved {}", h.path().display())),
|
||||||
|
Err(e) => Some(format!("Save failed: {e}")),
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AppMessage::AttachmentSaved,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn horizontal_space() -> iced::widget::Space {
|
fn horizontal_space() -> iced::widget::Space {
|
||||||
|
|||||||