add android icon
This commit is contained in:
1 parent
f22d954105
commit
55a69c743a
521 files changed
+8146
-14086
No files matched your search
+16
-28
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user