add android icon

This commit is contained in:
netadmin committed 2026-11-02 22:13:43 +03:00
1 parent f22d954105
commit 55a69c743a
521 files changed
+8146 -14086

No files matched your search

+16 -28
View File
@@ -34,30 +34,12 @@ const USAGE: &str = r#"ramshare_ui — обмен текстом и файлам
scan [диапазон] просканировать сеть и показать найденные устройства
(all, 192.168.1.1-254, 10.0.0.0/24, список через запятую)
peers показать, кто онлайн (известные + UDP-обнаружение)
config показать путь и значения config.json (создаёт файл при первом запуске)
config показать путь и значения config.json (включая media_dir)
trust <устройство> сверить код и заверить устройство (сверка взаимная: второе
устройство получит запрос и тоже должно подтвердить код)
untrust <устройство> отозвать доверие
send-text <устройство> <текст…|-> отправить текст ("-" — читать из stdin)
send-file <устройство> <файл…> отправить один или несколько файлов
<устройство> — имя, IP[:порт], префикс ID (≥4 hex) или полный отпечаток
Параметры:
--name <имя> имя устройства на этот запуск (постоянное задаётся в config.json)
--p2p-port <порт> порт P2P (HTTPS, mTLS), по умолчанию 8443; для scan/send — порт поиска
--ui-port <порт> порт локального веб-интерфейса (только 127.0.0.1), по умолчанию 8080
--ip <IPv4> адрес интерфейса (по умолчанию определяется автоматически)
--data-dir <путь> базовый каталог вместо папки с ramshare_ui; внутри создаются
config/ (config.json, trusted.json) и received_media/ (принятые файлы)
--ping-interval <с> интервал опроса известных устройств, 10–30 с (иначе config.json, иначе 15)
--no-ui (serve) без веб-интерфейса; входящий текст печатается в терминал
--wait <с> (CLI) сколько слушать UDP-обнаружение, 1–60, по умолчанию 4
--scan <диапазон> (CLI) если устройство не найдено — просканировать этот диапазон
--confirm-wait <с> (trust) ждать подтверждения на втором устройстве, 0–600 (по умолчанию 0)
--json (scan/peers) вывод в JSON
-y, --yes (trust) не спрашивать подтверждение (использовать осознанно!)
-h, --help эта справка
"#;
enum Command {
@@ -236,8 +218,7 @@ async fn run_serve(cfg: &Config, state: Arc<AppState>, bind_ip: Ipv4Addr) {
let server_cfg = tls::server_config(&state.cert_der, &state.key_der).unwrap_or_else(|e| fatal(&e));
let acceptor = tokio_rustls::TlsAcceptor::from(server_cfg);
let p2p_listener = tokio::net::TcpListener::bind((bind_ip, cfg.p2p_port))
.await
let p2p_listener = net::bind_p2p_listener(bind_ip, cfg.p2p_port)
.unwrap_or_else(|e| fatal(&format!("P2P-порт {}:{} недоступен: {}", bind_ip, cfg.p2p_port, e)));
let ui_listener = if cfg.no_ui {
None
@@ -257,7 +238,7 @@ async fn run_serve(cfg: &Config, state: Arc<AppState>, bind_ip: Ipv4Addr) {
short_id(&state.my_fingerprint)
));
net::start_discovery(state.clone());
let _discovery_handles = net::start_discovery(state.clone());
{
let st = state.clone();
@@ -283,7 +264,7 @@ async fn run_serve(cfg: &Config, state: Arc<AppState>, bind_ip: Ipv4Addr) {
println!(" ID устройства: {}", short_id(&state.my_fingerprint));
println!(" Папка config: {}", state.data_dir.display());
println!(" Файл настроек: {}", state.data_dir.join("config.json").display());
println!(" Принятые файлы: {}", state.media_dir.display());
println!(" Принятые файлы: {}", state.current_media_dir().display());
println!(
" Опрос устройств: каждые {} с (до {} попыток при недоступности)",
state.ping_interval_sec.load(Ordering::Relaxed),
@@ -303,7 +284,7 @@ async fn run_serve(cfg: &Config, state: Arc<AppState>, bind_ip: Ipv4Addr) {
}
None => {
println!(" Режим без UI (--no-ui). Входящий текст печатается в терминал,");
println!(" файлы сохраняются в {}", state.media_dir.display());
println!(" файлы сохраняются в {}", state.current_media_dir().display());
None
}
};
@@ -326,18 +307,16 @@ async fn run_serve(cfg: &Config, state: Arc<AppState>, bind_ip: Ipv4Addr) {
state.sync_trust_from_disk();
state.persist_trust();
storage::cleanup_partials(&state.media_dir);
storage::cleanup_partials(&state.current_media_dir());
}
#[tokio::main]
async fn main() {
let cfg = parse_args().unwrap_or_else(|e| fatal(&e));
let serving = matches!(cfg.command, Command::Serve);
let dirs = storage::resolve_dirs(cfg.data_dir.as_deref(), cfg.p2p_port).unwrap_or_else(|e| fatal(&e));
let (state, bind_ip) = init_app_state(
dirs.config,
dirs.media,
cfg.data_dir.as_deref(),
cfg.name.clone(),
&default_device_name(),
cfg.p2p_port,
@@ -371,6 +350,15 @@ async fn main() {
println!("Файл конфигурации: {}", state.data_dir.join("config.json").display());
println!("device_name: {}", state.device_name);
println!("ping_interval_sec: {}", state.ping_interval_sec.load(Ordering::Relaxed));
println!(
"media_dir (config): {}",
state
.configured_media_dir
.read()
.as_deref()
.unwrap_or("(по умолчанию — в папке запуска)")
);
println!("media_dir (активный): {}", state.current_media_dir().display());
println!("identity.fingerprint: {}", state.my_fingerprint);
Ok(())
}
+35 -15
View File
@@ -81,6 +81,10 @@ function renderHeader(d) {
pi.min = d.ping_min;
pi.max = d.ping_max;
if (document.activeElement !== pi) pi.value = d.ping_interval_sec;
const mdi = $('media-dir-input');
if (document.activeElement !== mdi) mdi.value = d.configured_media_dir || '';
if (d.my_ip && d.my_ip.includes('.') && !d.my_ip.startsWith('127.')) {
const p = d.my_ip.split('.');
const subnet = `${p[0]}.${p[1]}.${p[2]}`;
@@ -310,12 +314,17 @@ function addManualPeer() {
function saveSettings() {
return guarded(async () => {
const el = $('ping-interval-input');
const mdi = $('media-dir-input');
const v = parseInt(el.value, 10);
const min = Number(el.min) || 10, max = Number(el.max) || 30;
if (!Number.isInteger(v) || v < min || v > max) return showStatus(`Интервал должен быть от ${min} до ${max} секунд`, true);
await postJson('/ui/settings', { ping_interval_sec: v });
showStatus(`Опрос устройств каждые ${v} с`);
await postJson('/ui/settings', {
ping_interval_sec: v,
media_dir: mdi.value.trim(),
});
showStatus('Настройки сохранены в config.json');
el.blur();
mdi.blur();
fetchState().catch(() => {});
});
}
@@ -381,23 +390,33 @@ async function sendMedia() {
const fileInput = $('media-input');
const btn = $('send-media-btn');
if (!peer_id) return showStatus('Выберите активное устройство-получатель!', true);
if (!fileInput.files.length) return showStatus('Выберите файл!', true);
const file = fileInput.files[0];
const files = Array.from(fileInput.files || []);
if (!files.length) return showStatus('Выберите хотя бы один файл!', true);
btn.disabled = true;
let sentCount = 0;
try {
await api('/ui/send-media', {
method: 'POST',
headers: {
'X-Target-Id': peer_id,
'X-Filename': encodeURIComponent(file.name),
'Content-Type': 'application/octet-stream',
},
body: file,
});
for (let i = 0; i < files.length; i++) {
const file = files[i];
showStatus(`Отправка (${i + 1}/${files.length}): «${file.name}»…`);
await api('/ui/send-media', {
method: 'POST',
headers: {
'X-Target-Id': peer_id,
'X-Filename': encodeURIComponent(file.name),
'Content-Type': 'application/octet-stream',
},
body: file,
});
sentCount++;
}
fileInput.value = '';
showStatus(`Файл «${file.name}» передан`);
showStatus(
files.length === 1
? `✓ Файл «${files[0].name}» передан`
: `✓ Все файлы переданы (${sentCount} шт.)`
);
} catch (e) {
showStatus(e.message || String(e), true);
showStatus(`Ошибка (передано ${sentCount}/${files.length}): ${e.message || String(e)}`, true);
} finally {
btn.disabled = false;
fetchState().catch(() => {});
@@ -437,6 +456,7 @@ $('btn-ping').addEventListener('click', pingPeers);
$('btn-ping-trusted').addEventListener('click', pingPeers);
$('btn-save-settings').addEventListener('click', saveSettings);
$('ping-interval-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') saveSettings(); });
$('media-dir-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') saveSettings(); });
$('btn-manual').addEventListener('click', addManualPeer);
$('btn-scan-range').addEventListener('click', () => scanIpRange(false));
$('btn-scan-all').addEventListener('click', () => scanIpRange(true));
+15 -9
View File
@@ -115,14 +115,19 @@
</div>
</div>
<div class="row" style="align-items:center;justify-content:space-between;">
<div class="row" style="align-items:center;">
<label class="small muted" for="ping-interval-input">Опрос каждые</label>
<input type="number" id="ping-interval-input" min="10" max="30" step="1" style="width:74px;">
<span class="small muted">сек (10–30)</span>
<button class="btn-sm" id="btn-save-settings">Сохранить</button>
<div class="sub-box">
<div class="row" style="align-items:center;justify-content:space-between;">
<div class="row" style="align-items:center;">
<label class="small muted" for="ping-interval-input">Опрос каждые</label>
<input type="number" id="ping-interval-input" min="10" max="30" step="1" style="width:74px;">
<span class="small muted">сек (10–30)</span>
</div>
<div class="small muted" id="last-ping-info">Последний пинг: —</div>
</div>
<div class="row" style="flex-wrap:nowrap;">
<input type="text" id="media-dir-input" placeholder="Папка received_media (пусто — в папке запуска)">
<button class="btn-sm" style="white-space:nowrap;" id="btn-save-settings">Сохранить настройки</button>
</div>
<div class="small muted" id="last-ping-info">Последний пинг: —</div>
</div>
<div id="peers-list" class="col"></div>
@@ -138,8 +143,9 @@
<button id="btn-send-text">Отправить текст в ОЗУ</button>
<hr style="border:0;border-top:1px solid var(--border);width:100%;margin:4px 0;">
<input type="file" id="media-input">
<button class="btn-green" id="send-media-btn">Отправить файл на диск получателя</button>
<label class="small muted" for="media-input">Выберите один или несколько файлов:</label>
<input type="file" id="media-input" multiple>
<button class="btn-green" id="send-media-btn">Отправить выбранные файлы</button>
<div id="transfers-list" class="col"></div>
</div>