From a124dda9570ee094dca08569d3595c27da5e7295 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Thu, 23 Nov 2023 15:40:26 +0000 Subject: [PATCH 01/38] Move TorrentState to a folder --- crates/librqbit/src/{torrent_state.rs => torrent_state/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename crates/librqbit/src/{torrent_state.rs => torrent_state/mod.rs} (100%) diff --git a/crates/librqbit/src/torrent_state.rs b/crates/librqbit/src/torrent_state/mod.rs similarity index 100% rename from crates/librqbit/src/torrent_state.rs rename to crates/librqbit/src/torrent_state/mod.rs From 7cd6102c6ac1cd2d71d208bdc1b605c5fad51f3f Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Thu, 23 Nov 2023 15:41:40 +0000 Subject: [PATCH 02/38] Move TorrentState to a torrent_state/live --- crates/librqbit/src/torrent_state/live/mod.rs | 1440 ++++++++++++++++ crates/librqbit/src/torrent_state/mod.rs | 1441 +---------------- 2 files changed, 1442 insertions(+), 1439 deletions(-) create mode 100644 crates/librqbit/src/torrent_state/live/mod.rs diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs new file mode 100644 index 0000000..c0a84d3 --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -0,0 +1,1440 @@ +// The main logic of rqbit is here - connecting to peers, reading and writing messages +// to them, tracking peer state etc. +// +// ## Architecture +// There are many tasks cooperating to download the torrent. Tasks communicate both with message passing +// and shared memory. +// +// ### Shared locked state +// Shared state is access by almost all actors through RwLocks. +// +// There's one source of truth (TorrentStateLocked) for which chunks we have, need, and what peers are we waiting them from. +// +// Peer states that are important to the outsiders (tasks other than manage_peer) are in a sharded hash-map (DashMap) +// +// ### Tasks (actors) +// Peer adder task: +// - spawns new peers as they become known. It pulls them from a queue. The queue is filled in by DHT and torrent trackers. +// Also gets updated when peers are reconnecting after errors. +// +// Each peer has one main task "manage_peer". It's composed of 2 futures running as one task through tokio::select: +// - "manage_peer" - this talks to the peer over network and calls callbacks on PeerHandler. The callbacks are not async, +// and are supposed to finish quickly (apart from writing to disk, which is accounted for as "spawn_blocking"). +// - "peer_chunk_requester" - this continuously sends requests for chunks to the peer. +// it may steal chunks/pieces from other peers. +// +// ## Peer lifecycle +// State transitions: +// - queued (initial state) -> connected +// - connected -> live +// - ANY STATE -> dead (on error) +// - ANY STATE -> not_needed (when we don't need to talk to the peer anymore) +// +// When the peer dies, it's rescheduled with exponential backoff. +// +// > NOTE: deadlock notice: +// > peers and stateLocked are behind 2 different locks. +// > if you lock them in different order, this may deadlock. +// > +// > so don't lock them both at the same time at all, or at the worst lock them in the +// > same order (peers one first, then the global one). + +use std::{ + collections::HashMap, + fs::File, + net::SocketAddr, + path::PathBuf, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::{Duration, Instant}, +}; + +use anyhow::{bail, Context}; +use backoff::backoff::Backoff; +use buffers::{ByteBuf, ByteString}; +use clone_to_owned::CloneToOwned; +use dashmap::DashMap; +use futures::{stream::FuturesUnordered, StreamExt}; +use librqbit_core::{ + id20::Id20, + lengths::{ChunkInfo, Lengths, ValidPieceIndex}, + torrent_metainfo::TorrentMetaV1Info, +}; +use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use peer_binary_protocol::{ + extended::handshake::ExtendedHandshake, Handshake, Message, MessageOwned, Piece, Request, +}; +use serde::Serialize; +use sha1w::Sha1; +use tokio::{ + sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Notify, Semaphore, + }, + time::timeout, +}; +use tracing::{debug, error, info, span, trace, warn, Level}; + +use crate::{ + chunk_tracker::{ChunkMarkingResult, ChunkTracker}, + file_ops::FileOps, + peer_connection::{ + PeerConnection, PeerConnectionHandler, PeerConnectionOptions, WriterRequest, + }, + peer_state::{ + atomic_inc, AggregatePeerStatsAtomic, InflightRequest, LivePeerState, Peer, PeerCounters, + PeerRx, PeerState, PeerStatsFilter, PeerStatsSnapshot, PeerTx, SendMany, + }, + spawn_utils::{spawn, BlockingSpawner}, + type_aliases::{PeerHandle, BF}, +}; + +pub struct InflightPiece { + pub peer: PeerHandle, + pub started: Instant, +} + +#[derive(Default)] +pub struct PeerStates { + stats: AggregatePeerStatsAtomic, + states: DashMap, +} + +#[derive(Debug, Default, Serialize, PartialEq, Eq)] +pub struct AggregatePeerStats { + pub queued: usize, + pub connecting: usize, + pub live: usize, + pub seen: usize, + pub dead: usize, + pub not_needed: usize, +} + +impl<'a> From<&'a AggregatePeerStatsAtomic> for AggregatePeerStats { + fn from(s: &'a AggregatePeerStatsAtomic) -> Self { + let ordering = Ordering::Relaxed; + Self { + queued: s.queued.load(ordering) as usize, + connecting: s.connecting.load(ordering) as usize, + live: s.live.load(ordering) as usize, + seen: s.seen.load(ordering) as usize, + dead: s.dead.load(ordering) as usize, + not_needed: s.not_needed.load(ordering) as usize, + } + } +} + +impl PeerStates { + pub fn stats(&self) -> AggregatePeerStats { + AggregatePeerStats::from(&self.stats) + } + + pub fn add_if_not_seen(&self, addr: SocketAddr) -> Option { + use dashmap::mapref::entry::Entry; + match self.states.entry(addr) { + Entry::Occupied(_) => None, + Entry::Vacant(vac) => { + vac.insert(Default::default()); + atomic_inc(&self.stats.queued); + atomic_inc(&self.stats.seen); + Some(addr) + } + } + } + pub fn with_peer(&self, addr: PeerHandle, f: impl FnOnce(&Peer) -> R) -> Option { + self.states.get(&addr).map(|e| f(e.value())) + } + + pub fn with_peer_mut( + &self, + addr: PeerHandle, + reason: &'static str, + f: impl FnOnce(&mut Peer) -> R, + ) -> Option { + timeit(reason, || self.states.get_mut(&addr)) + .map(|e| f(TimedExistence::new(e, reason).value_mut())) + } + pub fn with_live(&self, addr: PeerHandle, f: impl FnOnce(&LivePeerState) -> R) -> Option { + self.states + .get(&addr) + .and_then(|e| match &e.value().state.get() { + PeerState::Live(l) => Some(f(l)), + _ => None, + }) + } + pub fn with_live_mut( + &self, + addr: PeerHandle, + reason: &'static str, + f: impl FnOnce(&mut LivePeerState) -> R, + ) -> Option { + self.with_peer_mut(addr, reason, |peer| peer.state.get_live_mut().map(f)) + .flatten() + } + + pub fn drop_peer(&self, handle: PeerHandle) -> Option { + let p = self.states.remove(&handle).map(|r| r.1)?; + self.stats.dec(p.state.get()); + Some(p) + } + + pub fn mark_peer_interested(&self, handle: PeerHandle, is_interested: bool) -> Option { + self.with_live_mut(handle, "mark_peer_interested", |live| { + let prev = live.peer_interested; + live.peer_interested = is_interested; + prev + }) + } + pub fn update_bitfield_from_vec(&self, handle: PeerHandle, bitfield: Vec) -> Option<()> { + self.with_live_mut(handle, "update_bitfield_from_vec", |live| { + live.bitfield = BF::from_vec(bitfield); + }) + } + pub fn mark_peer_connecting(&self, h: PeerHandle) -> anyhow::Result<(PeerRx, PeerTx)> { + let rx = self + .with_peer_mut(h, "mark_peer_connecting", |peer| { + peer.state + .queued_to_connecting(&self.stats) + .context("invalid peer state") + }) + .context("peer not found in states")??; + Ok(rx) + } + + fn reset_peer_backoff(&self, handle: PeerHandle) { + self.with_peer_mut(handle, "reset_peer_backoff", |p| { + p.stats.backoff.reset(); + }); + } + + fn mark_peer_not_needed(&self, handle: PeerHandle) -> Option { + let prev = self.with_peer_mut(handle, "mark_peer_not_needed", |peer| { + peer.state.to_not_needed(&self.stats) + })?; + Some(prev) + } +} + +pub struct TorrentStateLocked { + // What chunks we have and need. + pub chunks: ChunkTracker, + + // At a moment in time, we are expecting a piece from only one peer. + // inflight_pieces stores this information. + pub inflight_pieces: HashMap, +} + +#[derive(Default, Debug)] +struct AtomicStats { + have_bytes: AtomicU64, + downloaded_and_checked_bytes: AtomicU64, + downloaded_and_checked_pieces: AtomicU64, + uploaded_bytes: AtomicU64, + fetched_bytes: AtomicU64, + total_piece_download_ms: AtomicU64, +} + +impl AtomicStats { + fn average_piece_download_time(&self) -> Option { + let d = self.downloaded_and_checked_pieces.load(Ordering::Acquire); + let t = self.total_piece_download_ms.load(Ordering::Acquire); + if d == 0 { + return None; + } + Some(Duration::from_secs_f64(t as f64 / d as f64 / 1000f64)) + } +} + +#[derive(Debug, Serialize)] +pub struct StatsSnapshot { + pub have_bytes: u64, + pub downloaded_and_checked_bytes: u64, + pub downloaded_and_checked_pieces: u64, + pub fetched_bytes: u64, + pub uploaded_bytes: u64, + pub initially_needed_bytes: u64, + pub remaining_bytes: u64, + pub total_bytes: u64, + #[serde(skip)] + pub time: Instant, + pub total_piece_download_ms: u64, + pub peer_stats: AggregatePeerStats, +} + +impl StatsSnapshot { + pub fn average_piece_download_time(&self) -> Option { + let d = self.downloaded_and_checked_pieces; + let t = self.total_piece_download_ms; + if d == 0 { + return None; + } + Some(Duration::from_secs_f64(t as f64 / d as f64 / 1000f64)) + } +} + +#[derive(Default)] +pub struct TorrentStateOptions { + pub peer_connect_timeout: Option, + pub peer_read_write_timeout: Option, +} + +pub struct TorrentState { + peers: PeerStates, + info: TorrentMetaV1Info, + locked: Arc>, + files: Vec>>, + filenames: Vec, + info_hash: Id20, + peer_id: Id20, + lengths: Lengths, + needed_bytes: u64, + have_plus_needed_bytes: u64, + stats: AtomicStats, + options: TorrentStateOptions, + + // Limits how many active (occupying network resources) peers there are at a moment in time. + peer_semaphore: Semaphore, + + // The queue for peer manager to connect to them. + peer_queue_tx: UnboundedSender, + + finished_notify: Notify, +} + +// Used during debugging to see if some locks take too long. +#[cfg(not(feature = "timed_existence"))] +mod timed_existence { + use std::ops::{Deref, DerefMut}; + + pub struct TimedExistence(T); + + impl TimedExistence { + #[inline(always)] + pub fn new(object: T, _reason: &'static str) -> Self { + Self(object) + } + } + + impl Deref for TimedExistence { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for TimedExistence { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + #[inline(always)] + pub fn timeit(_n: impl std::fmt::Display, f: impl FnOnce() -> R) -> R { + f() + } +} + +#[cfg(feature = "timed_existence")] +mod timed_existence { + use std::ops::{Deref, DerefMut}; + use std::time::{Duration, Instant}; + use tracing::warn; + + const MAX: Duration = Duration::from_millis(1); + + // Prints if the object exists for too long. + // This is used to track long-lived locks for debugging. + pub struct TimedExistence { + object: T, + reason: &'static str, + started: Instant, + } + + impl TimedExistence { + pub fn new(object: T, reason: &'static str) -> Self { + Self { + object, + reason, + started: Instant::now(), + } + } + } + + impl Drop for TimedExistence { + fn drop(&mut self) { + let elapsed = self.started.elapsed(); + let reason = self.reason; + if elapsed > MAX { + warn!("elapsed on lock {reason:?}: {elapsed:?}") + } + } + } + + impl Deref for TimedExistence { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.object + } + } + + impl DerefMut for TimedExistence { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.object + } + } + + pub fn timeit(name: impl std::fmt::Display, f: impl FnOnce() -> R) -> R { + let now = Instant::now(); + let r = f(); + let elapsed = now.elapsed(); + if elapsed > MAX { + warn!("elapsed on \"{name:}\": {elapsed:?}") + } + r + } +} + +pub use timed_existence::{timeit, TimedExistence}; + +impl TorrentState { + #[allow(clippy::too_many_arguments)] + pub fn new( + info: TorrentMetaV1Info, + info_hash: Id20, + peer_id: Id20, + files: Vec>>, + filenames: Vec, + chunk_tracker: ChunkTracker, + lengths: Lengths, + have_bytes: u64, + needed_bytes: u64, + spawner: BlockingSpawner, + options: Option, + ) -> Arc { + let options = options.unwrap_or_default(); + let (peer_queue_tx, peer_queue_rx) = unbounded_channel(); + let state = Arc::new(TorrentState { + info_hash, + info, + peer_id, + peers: Default::default(), + locked: Arc::new(RwLock::new(TorrentStateLocked { + chunks: chunk_tracker, + inflight_pieces: Default::default(), + })), + files, + filenames, + stats: AtomicStats { + have_bytes: AtomicU64::new(have_bytes), + ..Default::default() + }, + needed_bytes, + have_plus_needed_bytes: needed_bytes + have_bytes, + lengths, + options, + + peer_semaphore: Semaphore::new(128), + peer_queue_tx, + finished_notify: Notify::new(), + }); + spawn( + span!(Level::ERROR, "peer_adder"), + state.clone().task_peer_adder(peer_queue_rx, spawner), + ); + state + } + + pub async fn task_manage_peer( + self: Arc, + addr: SocketAddr, + spawner: BlockingSpawner, + ) -> anyhow::Result<()> { + let state = self; + let (rx, tx) = state.peers.mark_peer_connecting(addr)?; + + let counters = state + .peers + .with_peer(addr, |p| p.stats.counters.clone()) + .context("bug: peer not found")?; + + let handler = PeerHandler { + addr, + on_bitfield_notify: Default::default(), + unchoke_notify: Default::default(), + locked: RwLock::new(PeerHandlerLocked { + i_am_choked: true, + previously_requested_pieces: BF::new(), + }), + requests_sem: Semaphore::new(0), + state: state.clone(), + tx, + spawner, + counters, + }; + let options = PeerConnectionOptions { + connect_timeout: state.options.peer_connect_timeout, + read_write_timeout: state.options.peer_read_write_timeout, + ..Default::default() + }; + let peer_connection = PeerConnection::new( + addr, + state.info_hash, + state.peer_id, + &handler, + Some(options), + spawner, + ); + let requester = handler.task_peer_chunk_requester(addr); + + handler + .counters + .connection_attempts + .fetch_add(1, Ordering::Relaxed); + let res = tokio::select! { + r = requester => {r} + r = peer_connection.manage_peer(rx) => {r} + }; + + handler.state.peer_semaphore.add_permits(1); + + match res { + // We disconnected the peer ourselves as we don't need it + Ok(()) => { + handler.on_peer_died(None); + } + Err(e) => { + debug!("error managing peer: {:#}", e); + handler.on_peer_died(Some(e)); + } + } + Ok::<_, anyhow::Error>(()) + } + + pub async fn task_peer_adder( + self: Arc, + mut peer_queue_rx: UnboundedReceiver, + spawner: BlockingSpawner, + ) -> anyhow::Result<()> { + let state = self; + loop { + let addr = peer_queue_rx.recv().await.unwrap(); + if state.is_finished() { + debug!("ignoring peer {} as we are finished", addr); + state.peers.mark_peer_not_needed(addr); + continue; + } + + let permit = state.peer_semaphore.acquire().await.unwrap(); + permit.forget(); + spawn( + span!(parent: None, Level::ERROR, "manage_peer", peer = addr.to_string()), + state.clone().task_manage_peer(addr, spawner), + ); + } + } + + pub fn info(&self) -> &TorrentMetaV1Info { + &self.info + } + pub fn info_hash(&self) -> Id20 { + self.info_hash + } + pub fn peer_id(&self) -> Id20 { + self.peer_id + } + pub fn file_ops(&self) -> FileOps<'_, Sha1> { + FileOps::new(&self.info, &self.files, &self.lengths) + } + pub fn initially_needed(&self) -> u64 { + self.needed_bytes + } + pub fn lock_read( + &self, + reason: &'static str, + ) -> TimedExistence> { + TimedExistence::new(timeit(reason, || self.locked.read()), reason) + } + pub fn lock_write( + &self, + reason: &'static str, + ) -> TimedExistence> { + TimedExistence::new(timeit(reason, || self.locked.write()), reason) + } + + fn get_next_needed_piece(&self, peer_handle: PeerHandle) -> Option { + self.peers + .with_live_mut(peer_handle, "l(get_next_needed_piece)", |live| { + let g = self.lock_read("g(get_next_needed_piece)"); + let bf = &live.bitfield; + for n in g.chunks.iter_needed_pieces() { + if bf.get(n).map(|v| *v) == Some(true) { + // in theory it should be safe without validation, but whatever. + return self.lengths.validate_piece_index(n as u32); + } + } + None + })? + } + + fn am_i_interested_in_peer(&self, handle: PeerHandle) -> bool { + self.get_next_needed_piece(handle).is_some() + } + + fn set_peer_live(&self, handle: PeerHandle, h: Handshake) { + let result = self.peers.with_peer_mut(handle, "set_peer_live", |p| { + p.state + .connecting_to_live(Id20(h.peer_id), &self.peers.stats) + .is_some() + }); + match result { + Some(true) => { + debug!("set peer to live") + } + Some(false) => debug!("can't set peer live, it was in wrong state"), + None => debug!("can't set peer live, it disappeared"), + } + } + + pub fn get_uploaded_bytes(&self) -> u64 { + self.stats.uploaded_bytes.load(Ordering::Relaxed) + } + pub fn get_downloaded_bytes(&self) -> u64 { + self.stats + .downloaded_and_checked_bytes + .load(Ordering::Acquire) + } + + pub fn is_finished(&self) -> bool { + self.get_left_to_download_bytes() == 0 + } + + pub fn get_left_to_download_bytes(&self) -> u64 { + self.needed_bytes - self.get_downloaded_bytes() + } + + fn maybe_transmit_haves(&self, index: ValidPieceIndex) { + let mut futures = Vec::new(); + + for pe in self.peers.states.iter() { + match &pe.value().state.get() { + PeerState::Live(live) => { + if !live.peer_interested { + continue; + } + + if live + .bitfield + .get(index.get() as usize) + .map(|v| *v) + .unwrap_or(false) + { + continue; + } + + let tx = live.tx.downgrade(); + futures.push(async move { + if let Some(tx) = tx.upgrade() { + if tx + .send(WriterRequest::Message(Message::Have(index.get()))) + .is_err() + { + // whatever + } + } + }); + } + _ => continue, + } + } + + if futures.is_empty() { + trace!("no peers to transmit Have={} to, saving some work", index); + return; + } + + let mut unordered: FuturesUnordered<_> = futures.into_iter().collect(); + spawn( + span!( + Level::ERROR, + "transmit_haves", + piece = index.get(), + count = unordered.len() + ), + async move { + while unordered.next().await.is_some() {} + Ok(()) + }, + ); + } + + pub fn add_peer_if_not_seen(self: &Arc, addr: SocketAddr) -> bool { + match self.peers.add_if_not_seen(addr) { + Some(handle) => handle, + None => return false, + }; + + self.peer_queue_tx.send(addr).unwrap(); + true + } + + pub fn stats_snapshot(&self) -> StatsSnapshot { + use Ordering::*; + let downloaded_bytes = self.stats.downloaded_and_checked_bytes.load(Relaxed); + let remaining = self.needed_bytes - downloaded_bytes; + StatsSnapshot { + have_bytes: self.stats.have_bytes.load(Relaxed), + downloaded_and_checked_bytes: downloaded_bytes, + downloaded_and_checked_pieces: self.stats.downloaded_and_checked_pieces.load(Relaxed), + fetched_bytes: self.stats.fetched_bytes.load(Relaxed), + uploaded_bytes: self.stats.uploaded_bytes.load(Relaxed), + total_bytes: self.have_plus_needed_bytes, + time: Instant::now(), + initially_needed_bytes: self.needed_bytes, + remaining_bytes: remaining, + total_piece_download_ms: self.stats.total_piece_download_ms.load(Relaxed), + peer_stats: self.peers.stats(), + } + } + + pub fn per_peer_stats_snapshot(&self, filter: PeerStatsFilter) -> PeerStatsSnapshot { + PeerStatsSnapshot { + peers: self + .peers + .states + .iter() + .filter(|e| filter.state.matches(e.value().state.get())) + .map(|e| (e.key().to_string(), e.value().into())) + .collect(), + } + } + + pub async fn wait_until_completed(&self) { + if self.is_finished() { + return; + } + self.finished_notify.notified().await; + } +} + +struct PeerHandlerLocked { + pub i_am_choked: bool, + + // This is used to only request a piece from a peer once when stealing from others. + // So that you don't steal then re-steal the same piece in a loop. + pub previously_requested_pieces: BF, +} + +// All peer state that would never be used by other actors should pe put here. +// This state tracks a live peer. +struct PeerHandler { + state: Arc, + counters: Arc, + // Semantically, we don't need an RwLock here, as this is only requested from + // one future (requester + manage_peer). + // + // However as PeerConnectionHandler takes &self everywhere, we need shared mutability. + // RefCell would do, but tokio is unhappy when we use it. + locked: RwLock, + + // This is used to unpause chunk requester once the bitfield + // is received. + on_bitfield_notify: Notify, + + // This is used to unpause after we were choked. + unchoke_notify: Notify, + + // This is used to limit the number of chunk requests we send to a peer at a time. + requests_sem: Semaphore, + + addr: SocketAddr, + spawner: BlockingSpawner, + + tx: PeerTx, +} + +impl<'a> PeerConnectionHandler for &'a PeerHandler { + fn on_connected(&self, connection_time: Duration) { + self.counters.connections.fetch_add(1, Ordering::Relaxed); + self.counters + .total_time_connecting_ms + .fetch_add(connection_time.as_millis() as u64, Ordering::Relaxed); + } + fn on_received_message(&self, message: Message>) -> anyhow::Result<()> { + match message { + Message::Request(request) => { + self.on_download_request(request) + .context("on_download_request")?; + } + Message::Bitfield(b) => self + .on_bitfield(b.clone_to_owned()) + .context("on_bitfield")?, + Message::Choke => self.on_i_am_choked(), + Message::Unchoke => self.on_i_am_unchoked(), + Message::Interested => self.on_peer_interested(), + Message::Piece(piece) => self.on_received_piece(piece).context("on_received_piece")?, + Message::KeepAlive => { + debug!("keepalive received"); + } + Message::Have(h) => self.on_have(h), + Message::NotInterested => { + info!("received \"not interested\", but we don't care yet") + } + message => { + warn!("received unsupported message {:?}, ignoring", message); + } + }; + Ok(()) + } + + fn get_have_bytes(&self) -> u64 { + self.state.stats.have_bytes.load(Ordering::Relaxed) + } + + fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec) -> Option { + let g = self.state.lock_read("serialize_bitfield_message_to_buf"); + let msg = Message::Bitfield(ByteBuf(g.chunks.get_have_pieces().as_raw_slice())); + let len = msg.serialize(buf, None).unwrap(); + debug!("sending: {:?}, length={}", &msg, len); + Some(len) + } + + fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> { + self.state.set_peer_live(self.addr, handshake); + Ok(()) + } + + fn on_uploaded_bytes(&self, bytes: u32) { + self.state + .stats + .uploaded_bytes + .fetch_add(bytes as u64, Ordering::Relaxed); + } + + fn read_chunk(&self, chunk: &ChunkInfo, buf: &mut [u8]) -> anyhow::Result<()> { + self.state.file_ops().read_chunk(self.addr, chunk, buf) + } + + fn on_extended_handshake(&self, _: &ExtendedHandshake) -> anyhow::Result<()> { + Ok(()) + } +} + +impl PeerHandler { + fn on_peer_died(self, error: Option) { + let peers = &self.state.peers; + let pstats = &peers.stats; + let handle = self.addr; + let mut pe = match peers.states.get_mut(&handle) { + Some(peer) => TimedExistence::new(peer, "on_peer_died"), + None => { + warn!("bug: peer not found in table. Forgetting it forever"); + return; + } + }; + let prev = pe.value_mut().state.take(pstats); + + match prev { + PeerState::Connecting(_) => {} + PeerState::Live(live) => { + let mut g = self.state.lock_write("mark_chunk_requests_canceled"); + for req in live.inflight_requests { + debug!( + "peer dead, marking chunk request cancelled, index={}, chunk={}", + req.piece.get(), + req.chunk + ); + g.chunks.mark_chunk_request_cancelled(req.piece, req.chunk); + } + } + PeerState::NotNeeded => { + // Restore it as std::mem::take() replaced it above. + pe.value_mut().state.set(PeerState::NotNeeded, pstats); + return; + } + s @ PeerState::Queued | s @ PeerState::Dead => { + warn!("bug: peer was in a wrong state {s:?}, ignoring it forever"); + // Prevent deadlocks. + drop(pe); + self.state.peers.drop_peer(handle); + return; + } + }; + + let _error = match error { + Some(e) => e, + None => { + debug!("peer died without errors, not re-queueing"); + pe.value_mut().state.set(PeerState::NotNeeded, pstats); + return; + } + }; + + self.counters.errors.fetch_add(1, Ordering::Relaxed); + + if self.state.is_finished() { + debug!("torrent finished, not re-queueing"); + pe.value_mut().state.set(PeerState::NotNeeded, pstats); + return; + } + + pe.value_mut().state.set(PeerState::Dead, pstats); + + let backoff = pe.value_mut().stats.backoff.next_backoff(); + + // Prevent deadlocks. + drop(pe); + + if let Some(dur) = backoff { + spawn( + span!( + parent: None, + Level::ERROR, + "wait_for_peer", + peer = handle.to_string(), + duration = format!("{dur:?}") + ), + async move { + tokio::time::sleep(dur).await; + self.state + .peers + .with_peer_mut(handle, "dead_to_queued", |peer| { + match peer.state.get() { + PeerState::Dead => { + peer.state.set(PeerState::Queued, &self.state.peers.stats) + } + other => bail!( + "peer is in unexpected state: {}. Expected dead", + other.name() + ), + }; + Ok(()) + }) + .context("bug: peer disappeared")??; + self.state.peer_queue_tx.send(handle)?; + Ok::<_, anyhow::Error>(()) + }, + ); + } else { + debug!("dropping peer, backoff exhausted"); + self.state.peers.drop_peer(handle); + } + } + + fn reserve_next_needed_piece(&self) -> Option { + // TODO: locking one inside the other in different order results in deadlocks. + self.state + .peers + .with_live_mut(self.addr, "reserve_next_needed_piece", |live| { + if self.locked.read().i_am_choked { + debug!("we are choked, can't reserve next piece"); + return None; + } + let mut g = self.state.lock_write("reserve_next_needed_piece"); + + let n = { + let mut n_opt = None; + let bf = &live.bitfield; + for n in g.chunks.iter_needed_pieces() { + if bf.get(n).map(|v| *v) == Some(true) { + n_opt = Some(n); + break; + } + } + + self.state.lengths.validate_piece_index(n_opt? as u32)? + }; + g.inflight_pieces.insert( + n, + InflightPiece { + peer: self.addr, + started: Instant::now(), + }, + ); + g.chunks.reserve_needed_piece(n); + Some(n) + }) + .flatten() + } + + fn try_steal_old_slow_piece(&self, threshold: f64) -> Option { + let total = self + .state + .stats + .downloaded_and_checked_pieces + .load(Ordering::Acquire); + + // heuristic for not enough precision in average time + if total < 20 { + return None; + } + let avg_time = self.state.stats.average_piece_download_time()?; + + let mut g = self.state.lock_write("try_steal_old_slow_piece"); + let (idx, elapsed, piece_req) = g + .inflight_pieces + .iter_mut() + // don't steal from myself + .filter(|(_, r)| r.peer != self.addr) + .map(|(p, r)| (p, r.started.elapsed(), r)) + .max_by_key(|(_, e, _)| *e)?; + + // heuristic for "too slow peer" + if elapsed.as_secs_f64() > avg_time.as_secs_f64() * threshold { + debug!( + "will steal piece {} from {}: elapsed time {:?}, avg piece time: {:?}", + idx, piece_req.peer, elapsed, avg_time + ); + piece_req.peer = self.addr; + piece_req.started = Instant::now(); + return Some(*idx); + } + None + } + + fn on_download_request(&self, request: Request) -> anyhow::Result<()> { + let piece_index = match self.state.lengths.validate_piece_index(request.index) { + Some(p) => p, + None => { + anyhow::bail!( + "received {:?}, but it is not a valid chunk request (piece index is invalid). Ignoring.", + request + ); + } + }; + let chunk_info = match self.state.lengths.chunk_info_from_received_data( + piece_index, + request.begin, + request.length, + ) { + Some(d) => d, + None => { + anyhow::bail!( + "received {:?}, but it is not a valid chunk request (chunk data is invalid). Ignoring.", + request + ); + } + }; + + if !self + .state + .lock_read("is_chunk_ready_to_upload") + .chunks + .is_chunk_ready_to_upload(&chunk_info) + { + anyhow::bail!( + "got request for a chunk that is not ready to upload. chunk {:?}", + &chunk_info + ); + } + + // TODO: this is not super efficient as it does copying multiple times. + // Theoretically, this could be done in the sending code, so that it reads straight into + // the send buffer. + let request = WriterRequest::ReadChunkRequest(chunk_info); + debug!("sending {:?}", &request); + Ok::<_, anyhow::Error>(self.tx.send(request)?) + } + + fn on_have(&self, have: u32) { + self.state + .peers + .with_live_mut(self.addr, "on_have", |live| { + // If bitfield wasn't allocated yet, let's do it. Some clients send haves before bitfield. + if live.bitfield.is_empty() { + live.bitfield = + BF::from_vec(vec![0; self.state.lengths.piece_bitfield_bytes()]); + } + match live.bitfield.get_mut(have as usize) { + Some(mut v) => *v = true, + None => { + warn!("received have {} out of range", have); + return; + } + }; + debug!("updated bitfield with have={}", have); + }); + } + + fn on_bitfield(&self, bitfield: ByteString) -> anyhow::Result<()> { + if bitfield.len() != self.state.lengths.piece_bitfield_bytes() { + anyhow::bail!( + "dropping peer as its bitfield has unexpected size. Got {}, expected {}", + bitfield.len(), + self.state.lengths.piece_bitfield_bytes(), + ); + } + self.locked.write().previously_requested_pieces = BF::from_vec(vec![0; bitfield.len()]); + self.state + .peers + .update_bitfield_from_vec(self.addr, bitfield.0); + + if !self.state.am_i_interested_in_peer(self.addr) { + self.tx + .send(WriterRequest::Message(MessageOwned::Unchoke))?; + self.tx + .send(WriterRequest::Message(MessageOwned::NotInterested))?; + if self.state.is_finished() { + self.tx.send(WriterRequest::Disconnect)?; + } + return Ok(()); + } + + self.on_bitfield_notify.notify_waiters(); + Ok(()) + } + + async fn task_peer_chunk_requester(&self, handle: PeerHandle) -> anyhow::Result<()> { + self.on_bitfield_notify.notified().await; + self.tx.send_many([ + WriterRequest::Message(MessageOwned::Unchoke), + WriterRequest::Message(MessageOwned::Interested), + ])?; + + #[allow(unused_must_use)] + { + timeout(Duration::from_secs(60), self.unchoke_notify.notified()).await; + } + + loop { + if self.locked.read().i_am_choked { + debug!("we are choked, can't reserve next piece"); + #[allow(unused_must_use)] + { + timeout(Duration::from_secs(60), self.unchoke_notify.notified()).await; + } + continue; + } + + if self.state.is_finished() { + debug!("nothing left to download, looping forever until manage_peer quits"); + loop { + tokio::time::sleep(Duration::from_secs(86400)).await; + } + } + + // Try steal a pice from a very slow peer first. Otherwise we might wait too long + // to download early pieces. + // Then try get the next one in queue. + // Afterwards means we are close to completion, try stealing more aggressively. + let next = match self + .try_steal_old_slow_piece(10.) + .or_else(|| self.reserve_next_needed_piece()) + .or_else(|| self.try_steal_old_slow_piece(2.)) + { + Some(next) => next, + None => { + debug!("no pieces to request"); + tokio::time::sleep(Duration::from_secs(10)).await; + continue; + } + }; + + self.locked + .write() + .previously_requested_pieces + .set(next.get() as usize, true); + + for chunk in self.state.lengths.iter_chunk_infos(next) { + let request = Request { + index: next.get(), + begin: chunk.offset, + length: chunk.size, + }; + + match self + .state + .peers + .with_live_mut(handle, "add chunk request", |live| { + live.inflight_requests.insert(InflightRequest::from(&chunk)) + }) { + Some(true) => {} + Some(false) => { + // This request was already in-flight for this peer for this chunk. + // This might happen in theory, but not very likely. + // + // Example: + // someone stole a piece from us, and then died, the piece became "needed" again, and we reserved it + // all before the piece request was processed by us. + warn!("we already requested {:?} previously", chunk); + continue; + } + // peer died + None => return Ok(()), + }; + + loop { + match timeout(Duration::from_secs(10), self.requests_sem.acquire()).await { + Ok(acq) => break acq?.forget(), + Err(_) => continue, + }; + } + + if self + .tx + .send(WriterRequest::Message(MessageOwned::Request(request))) + .is_err() + { + return Ok(()); + } + } + } + } + + fn on_i_am_choked(&self) { + self.locked.write().i_am_choked = true; + } + + fn on_peer_interested(&self) { + debug!("peer is interested"); + self.state.peers.mark_peer_interested(self.addr, true); + } + + fn reopen_read_only(&self) -> anyhow::Result<()> { + fn dummy_file() -> anyhow::Result { + #[cfg(target_os = "windows")] + const DEVNULL: &str = "NUL"; + #[cfg(not(target_os = "windows"))] + const DEVNULL: &str = "/dev/null"; + + std::fs::OpenOptions::new() + .read(true) + .open(DEVNULL) + .with_context(|| format!("error opening {}", DEVNULL)) + } + + // Lock exclusive just in case to ensure in-flight operations finish.?? + let _guard = self.state.lock_write("reopen_read_only"); + + for (file, filename) in self.state.files.iter().zip(self.state.filenames.iter()) { + let mut g = file.lock(); + // this should close the original file + // putting in a block just in case to guarantee drop. + { + *g = dummy_file()?; + } + *g = std::fs::OpenOptions::new() + .read(true) + .open(filename) + .with_context(|| format!("error re-opening {:?} readonly", filename))?; + debug!("reopened {:?} read-only", filename); + } + info!("reopened all torrent files in read-only mode"); + Ok(()) + } + + fn on_i_am_unchoked(&self) { + debug!("we are unchoked"); + self.locked.write().i_am_choked = false; + self.unchoke_notify.notify_waiters(); + self.requests_sem.add_permits(16); + } + + fn on_received_piece(&self, piece: Piece) -> anyhow::Result<()> { + let chunk_info = match self.state.lengths.chunk_info_from_received_piece( + piece.index, + piece.begin, + piece.block.len() as u32, + ) { + Some(i) => i, + None => { + anyhow::bail!("peer sent us an invalid piece {:?}", &piece,); + } + }; + + self.requests_sem.add_permits(1); + + // Peer chunk/byte counters. + self.counters + .fetched_bytes + .fetch_add(piece.block.len() as u64, Ordering::Relaxed); + self.counters.fetched_chunks.fetch_add(1, Ordering::Relaxed); + + // Global chunk/byte counters. + self.state + .stats + .fetched_bytes + .fetch_add(piece.block.len() as u64, Ordering::Relaxed); + + self.state + .peers + .with_live_mut(self.addr, "inflight_requests.remove", |h| { + if !h + .inflight_requests + .remove(&InflightRequest::from(&chunk_info)) + { + anyhow::bail!( + "peer sent us a piece we did not ask. Requested pieces: {:?}. Got: {:?}", + &h.inflight_requests, + &piece, + ); + } + Ok(()) + }) + .context("peer not found")??; + + let full_piece_download_time = { + let mut g = self.state.lock_write("mark_chunk_downloaded"); + + match g.inflight_pieces.get(&chunk_info.piece_index) { + Some(InflightPiece { peer, .. }) if *peer == self.addr => {} + Some(InflightPiece { peer, .. }) => { + debug!( + "in-flight piece {} was stolen by {}, ignoring", + chunk_info.piece_index, peer + ); + return Ok(()); + } + None => { + debug!( + "in-flight piece {} not found. it was probably completed by someone else", + chunk_info.piece_index + ); + return Ok(()); + } + }; + + match g.chunks.mark_chunk_downloaded(&piece) { + Some(ChunkMarkingResult::Completed) => { + debug!("piece={} done, will write and checksum", piece.index,); + // This will prevent others from stealing it. + { + let piece = chunk_info.piece_index; + g.inflight_pieces.remove(&piece) + } + .map(|t| t.started.elapsed()) + } + Some(ChunkMarkingResult::PreviouslyCompleted) => { + // TODO: we might need to send cancellations here. + debug!("piece={} was done by someone else, ignoring", piece.index,); + return Ok(()); + } + Some(ChunkMarkingResult::NotCompleted) => None, + None => { + anyhow::bail!( + "bogus data received: {:?}, cannot map this to a chunk, dropping peer", + piece + ); + } + } + }; + + // By this time we reach here, no other peer can for this piece. All others, even if they steal pieces would + // have fallen off above in one of the defensive checks. + + self.spawner + .spawn_block_in_place(move || { + let index = piece.index; + + // TODO: in theory we should unmark the piece as downloaded here. But if there was a disk error, what + // should we really do? If we unmark it, it will get requested forever... + // + // So let's just unwrap and abort. + match self + .state + .file_ops() + .write_chunk(self.addr, &piece, &chunk_info) + { + Ok(()) => {} + Err(e) => { + error!("FATAL: error writing chunk to disk: {:?}", e); + panic!("{:?}", e); + } + } + + let full_piece_download_time = match full_piece_download_time { + Some(t) => t, + None => return Ok(()), + }; + + match self + .state + .file_ops() + .check_piece(self.addr, chunk_info.piece_index, &chunk_info) + .with_context(|| format!("error checking piece={index}"))? + { + true => { + { + let mut g = self.state.lock_write("mark_piece_downloaded"); + g.chunks.mark_piece_downloaded(chunk_info.piece_index); + } + + // Global piece counters. + let piece_len = + self.state.lengths.piece_length(chunk_info.piece_index) as u64; + self.state + .stats + .downloaded_and_checked_bytes + // This counter is used to compute "is_finished", so using + // stronger ordering. + .fetch_add(piece_len, Ordering::Release); + self.state + .stats + .downloaded_and_checked_pieces + // This counter is used to compute "is_finished", so using + // stronger ordering. + .fetch_add(1, Ordering::Release); + self.state + .stats + .have_bytes + .fetch_add(piece_len, Ordering::Relaxed); + self.state.stats.total_piece_download_ms.fetch_add( + full_piece_download_time.as_millis() as u64, + Ordering::Release, + ); + + // Per-peer piece counters. + self.counters + .downloaded_and_checked_pieces + .fetch_add(1, Ordering::Relaxed); + self.counters + .downloaded_and_checked_bytes + .fetch_add(piece_len, Ordering::Relaxed); + + self.state.peers.reset_peer_backoff(self.addr); + + debug!("piece={} successfully downloaded and verified", index); + + if self.state.is_finished() { + info!("torrent finished downloading"); + self.state.finished_notify.notify_waiters(); + self.disconnect_all_peers_that_have_full_torrent(); + self.reopen_read_only()?; + } + + self.state.maybe_transmit_haves(chunk_info.piece_index); + } + false => { + warn!("checksum for piece={} did not validate", index,); + self.state + .lock_write("mark_piece_broken") + .chunks + .mark_piece_broken(chunk_info.piece_index); + } + }; + Ok::<_, anyhow::Error>(()) + }) + .with_context(|| format!("error processing received chunk {chunk_info:?}"))?; + Ok(()) + } + + fn disconnect_all_peers_that_have_full_torrent(&self) { + for mut pe in self.state.peers.states.iter_mut() { + if let PeerState::Live(l) = pe.value().state.get() { + if l.has_full_torrent(self.state.lengths.total_pieces() as usize) { + let prev = pe.value_mut().state.to_not_needed(&self.state.peers.stats); + let _ = prev + .take_live_no_counters() + .unwrap() + .tx + .send(WriterRequest::Disconnect); + } + } + } + } +} diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index c0a84d3..0ac1567 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -1,1440 +1,3 @@ -// The main logic of rqbit is here - connecting to peers, reading and writing messages -// to them, tracking peer state etc. -// -// ## Architecture -// There are many tasks cooperating to download the torrent. Tasks communicate both with message passing -// and shared memory. -// -// ### Shared locked state -// Shared state is access by almost all actors through RwLocks. -// -// There's one source of truth (TorrentStateLocked) for which chunks we have, need, and what peers are we waiting them from. -// -// Peer states that are important to the outsiders (tasks other than manage_peer) are in a sharded hash-map (DashMap) -// -// ### Tasks (actors) -// Peer adder task: -// - spawns new peers as they become known. It pulls them from a queue. The queue is filled in by DHT and torrent trackers. -// Also gets updated when peers are reconnecting after errors. -// -// Each peer has one main task "manage_peer". It's composed of 2 futures running as one task through tokio::select: -// - "manage_peer" - this talks to the peer over network and calls callbacks on PeerHandler. The callbacks are not async, -// and are supposed to finish quickly (apart from writing to disk, which is accounted for as "spawn_blocking"). -// - "peer_chunk_requester" - this continuously sends requests for chunks to the peer. -// it may steal chunks/pieces from other peers. -// -// ## Peer lifecycle -// State transitions: -// - queued (initial state) -> connected -// - connected -> live -// - ANY STATE -> dead (on error) -// - ANY STATE -> not_needed (when we don't need to talk to the peer anymore) -// -// When the peer dies, it's rescheduled with exponential backoff. -// -// > NOTE: deadlock notice: -// > peers and stateLocked are behind 2 different locks. -// > if you lock them in different order, this may deadlock. -// > -// > so don't lock them both at the same time at all, or at the worst lock them in the -// > same order (peers one first, then the global one). +pub mod live; -use std::{ - collections::HashMap, - fs::File, - net::SocketAddr, - path::PathBuf, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, - time::{Duration, Instant}, -}; - -use anyhow::{bail, Context}; -use backoff::backoff::Backoff; -use buffers::{ByteBuf, ByteString}; -use clone_to_owned::CloneToOwned; -use dashmap::DashMap; -use futures::{stream::FuturesUnordered, StreamExt}; -use librqbit_core::{ - id20::Id20, - lengths::{ChunkInfo, Lengths, ValidPieceIndex}, - torrent_metainfo::TorrentMetaV1Info, -}; -use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use peer_binary_protocol::{ - extended::handshake::ExtendedHandshake, Handshake, Message, MessageOwned, Piece, Request, -}; -use serde::Serialize; -use sha1w::Sha1; -use tokio::{ - sync::{ - mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - Notify, Semaphore, - }, - time::timeout, -}; -use tracing::{debug, error, info, span, trace, warn, Level}; - -use crate::{ - chunk_tracker::{ChunkMarkingResult, ChunkTracker}, - file_ops::FileOps, - peer_connection::{ - PeerConnection, PeerConnectionHandler, PeerConnectionOptions, WriterRequest, - }, - peer_state::{ - atomic_inc, AggregatePeerStatsAtomic, InflightRequest, LivePeerState, Peer, PeerCounters, - PeerRx, PeerState, PeerStatsFilter, PeerStatsSnapshot, PeerTx, SendMany, - }, - spawn_utils::{spawn, BlockingSpawner}, - type_aliases::{PeerHandle, BF}, -}; - -pub struct InflightPiece { - pub peer: PeerHandle, - pub started: Instant, -} - -#[derive(Default)] -pub struct PeerStates { - stats: AggregatePeerStatsAtomic, - states: DashMap, -} - -#[derive(Debug, Default, Serialize, PartialEq, Eq)] -pub struct AggregatePeerStats { - pub queued: usize, - pub connecting: usize, - pub live: usize, - pub seen: usize, - pub dead: usize, - pub not_needed: usize, -} - -impl<'a> From<&'a AggregatePeerStatsAtomic> for AggregatePeerStats { - fn from(s: &'a AggregatePeerStatsAtomic) -> Self { - let ordering = Ordering::Relaxed; - Self { - queued: s.queued.load(ordering) as usize, - connecting: s.connecting.load(ordering) as usize, - live: s.live.load(ordering) as usize, - seen: s.seen.load(ordering) as usize, - dead: s.dead.load(ordering) as usize, - not_needed: s.not_needed.load(ordering) as usize, - } - } -} - -impl PeerStates { - pub fn stats(&self) -> AggregatePeerStats { - AggregatePeerStats::from(&self.stats) - } - - pub fn add_if_not_seen(&self, addr: SocketAddr) -> Option { - use dashmap::mapref::entry::Entry; - match self.states.entry(addr) { - Entry::Occupied(_) => None, - Entry::Vacant(vac) => { - vac.insert(Default::default()); - atomic_inc(&self.stats.queued); - atomic_inc(&self.stats.seen); - Some(addr) - } - } - } - pub fn with_peer(&self, addr: PeerHandle, f: impl FnOnce(&Peer) -> R) -> Option { - self.states.get(&addr).map(|e| f(e.value())) - } - - pub fn with_peer_mut( - &self, - addr: PeerHandle, - reason: &'static str, - f: impl FnOnce(&mut Peer) -> R, - ) -> Option { - timeit(reason, || self.states.get_mut(&addr)) - .map(|e| f(TimedExistence::new(e, reason).value_mut())) - } - pub fn with_live(&self, addr: PeerHandle, f: impl FnOnce(&LivePeerState) -> R) -> Option { - self.states - .get(&addr) - .and_then(|e| match &e.value().state.get() { - PeerState::Live(l) => Some(f(l)), - _ => None, - }) - } - pub fn with_live_mut( - &self, - addr: PeerHandle, - reason: &'static str, - f: impl FnOnce(&mut LivePeerState) -> R, - ) -> Option { - self.with_peer_mut(addr, reason, |peer| peer.state.get_live_mut().map(f)) - .flatten() - } - - pub fn drop_peer(&self, handle: PeerHandle) -> Option { - let p = self.states.remove(&handle).map(|r| r.1)?; - self.stats.dec(p.state.get()); - Some(p) - } - - pub fn mark_peer_interested(&self, handle: PeerHandle, is_interested: bool) -> Option { - self.with_live_mut(handle, "mark_peer_interested", |live| { - let prev = live.peer_interested; - live.peer_interested = is_interested; - prev - }) - } - pub fn update_bitfield_from_vec(&self, handle: PeerHandle, bitfield: Vec) -> Option<()> { - self.with_live_mut(handle, "update_bitfield_from_vec", |live| { - live.bitfield = BF::from_vec(bitfield); - }) - } - pub fn mark_peer_connecting(&self, h: PeerHandle) -> anyhow::Result<(PeerRx, PeerTx)> { - let rx = self - .with_peer_mut(h, "mark_peer_connecting", |peer| { - peer.state - .queued_to_connecting(&self.stats) - .context("invalid peer state") - }) - .context("peer not found in states")??; - Ok(rx) - } - - fn reset_peer_backoff(&self, handle: PeerHandle) { - self.with_peer_mut(handle, "reset_peer_backoff", |p| { - p.stats.backoff.reset(); - }); - } - - fn mark_peer_not_needed(&self, handle: PeerHandle) -> Option { - let prev = self.with_peer_mut(handle, "mark_peer_not_needed", |peer| { - peer.state.to_not_needed(&self.stats) - })?; - Some(prev) - } -} - -pub struct TorrentStateLocked { - // What chunks we have and need. - pub chunks: ChunkTracker, - - // At a moment in time, we are expecting a piece from only one peer. - // inflight_pieces stores this information. - pub inflight_pieces: HashMap, -} - -#[derive(Default, Debug)] -struct AtomicStats { - have_bytes: AtomicU64, - downloaded_and_checked_bytes: AtomicU64, - downloaded_and_checked_pieces: AtomicU64, - uploaded_bytes: AtomicU64, - fetched_bytes: AtomicU64, - total_piece_download_ms: AtomicU64, -} - -impl AtomicStats { - fn average_piece_download_time(&self) -> Option { - let d = self.downloaded_and_checked_pieces.load(Ordering::Acquire); - let t = self.total_piece_download_ms.load(Ordering::Acquire); - if d == 0 { - return None; - } - Some(Duration::from_secs_f64(t as f64 / d as f64 / 1000f64)) - } -} - -#[derive(Debug, Serialize)] -pub struct StatsSnapshot { - pub have_bytes: u64, - pub downloaded_and_checked_bytes: u64, - pub downloaded_and_checked_pieces: u64, - pub fetched_bytes: u64, - pub uploaded_bytes: u64, - pub initially_needed_bytes: u64, - pub remaining_bytes: u64, - pub total_bytes: u64, - #[serde(skip)] - pub time: Instant, - pub total_piece_download_ms: u64, - pub peer_stats: AggregatePeerStats, -} - -impl StatsSnapshot { - pub fn average_piece_download_time(&self) -> Option { - let d = self.downloaded_and_checked_pieces; - let t = self.total_piece_download_ms; - if d == 0 { - return None; - } - Some(Duration::from_secs_f64(t as f64 / d as f64 / 1000f64)) - } -} - -#[derive(Default)] -pub struct TorrentStateOptions { - pub peer_connect_timeout: Option, - pub peer_read_write_timeout: Option, -} - -pub struct TorrentState { - peers: PeerStates, - info: TorrentMetaV1Info, - locked: Arc>, - files: Vec>>, - filenames: Vec, - info_hash: Id20, - peer_id: Id20, - lengths: Lengths, - needed_bytes: u64, - have_plus_needed_bytes: u64, - stats: AtomicStats, - options: TorrentStateOptions, - - // Limits how many active (occupying network resources) peers there are at a moment in time. - peer_semaphore: Semaphore, - - // The queue for peer manager to connect to them. - peer_queue_tx: UnboundedSender, - - finished_notify: Notify, -} - -// Used during debugging to see if some locks take too long. -#[cfg(not(feature = "timed_existence"))] -mod timed_existence { - use std::ops::{Deref, DerefMut}; - - pub struct TimedExistence(T); - - impl TimedExistence { - #[inline(always)] - pub fn new(object: T, _reason: &'static str) -> Self { - Self(object) - } - } - - impl Deref for TimedExistence { - type Target = T; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl DerefMut for TimedExistence { - #[inline(always)] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - #[inline(always)] - pub fn timeit(_n: impl std::fmt::Display, f: impl FnOnce() -> R) -> R { - f() - } -} - -#[cfg(feature = "timed_existence")] -mod timed_existence { - use std::ops::{Deref, DerefMut}; - use std::time::{Duration, Instant}; - use tracing::warn; - - const MAX: Duration = Duration::from_millis(1); - - // Prints if the object exists for too long. - // This is used to track long-lived locks for debugging. - pub struct TimedExistence { - object: T, - reason: &'static str, - started: Instant, - } - - impl TimedExistence { - pub fn new(object: T, reason: &'static str) -> Self { - Self { - object, - reason, - started: Instant::now(), - } - } - } - - impl Drop for TimedExistence { - fn drop(&mut self) { - let elapsed = self.started.elapsed(); - let reason = self.reason; - if elapsed > MAX { - warn!("elapsed on lock {reason:?}: {elapsed:?}") - } - } - } - - impl Deref for TimedExistence { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.object - } - } - - impl DerefMut for TimedExistence { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.object - } - } - - pub fn timeit(name: impl std::fmt::Display, f: impl FnOnce() -> R) -> R { - let now = Instant::now(); - let r = f(); - let elapsed = now.elapsed(); - if elapsed > MAX { - warn!("elapsed on \"{name:}\": {elapsed:?}") - } - r - } -} - -pub use timed_existence::{timeit, TimedExistence}; - -impl TorrentState { - #[allow(clippy::too_many_arguments)] - pub fn new( - info: TorrentMetaV1Info, - info_hash: Id20, - peer_id: Id20, - files: Vec>>, - filenames: Vec, - chunk_tracker: ChunkTracker, - lengths: Lengths, - have_bytes: u64, - needed_bytes: u64, - spawner: BlockingSpawner, - options: Option, - ) -> Arc { - let options = options.unwrap_or_default(); - let (peer_queue_tx, peer_queue_rx) = unbounded_channel(); - let state = Arc::new(TorrentState { - info_hash, - info, - peer_id, - peers: Default::default(), - locked: Arc::new(RwLock::new(TorrentStateLocked { - chunks: chunk_tracker, - inflight_pieces: Default::default(), - })), - files, - filenames, - stats: AtomicStats { - have_bytes: AtomicU64::new(have_bytes), - ..Default::default() - }, - needed_bytes, - have_plus_needed_bytes: needed_bytes + have_bytes, - lengths, - options, - - peer_semaphore: Semaphore::new(128), - peer_queue_tx, - finished_notify: Notify::new(), - }); - spawn( - span!(Level::ERROR, "peer_adder"), - state.clone().task_peer_adder(peer_queue_rx, spawner), - ); - state - } - - pub async fn task_manage_peer( - self: Arc, - addr: SocketAddr, - spawner: BlockingSpawner, - ) -> anyhow::Result<()> { - let state = self; - let (rx, tx) = state.peers.mark_peer_connecting(addr)?; - - let counters = state - .peers - .with_peer(addr, |p| p.stats.counters.clone()) - .context("bug: peer not found")?; - - let handler = PeerHandler { - addr, - on_bitfield_notify: Default::default(), - unchoke_notify: Default::default(), - locked: RwLock::new(PeerHandlerLocked { - i_am_choked: true, - previously_requested_pieces: BF::new(), - }), - requests_sem: Semaphore::new(0), - state: state.clone(), - tx, - spawner, - counters, - }; - let options = PeerConnectionOptions { - connect_timeout: state.options.peer_connect_timeout, - read_write_timeout: state.options.peer_read_write_timeout, - ..Default::default() - }; - let peer_connection = PeerConnection::new( - addr, - state.info_hash, - state.peer_id, - &handler, - Some(options), - spawner, - ); - let requester = handler.task_peer_chunk_requester(addr); - - handler - .counters - .connection_attempts - .fetch_add(1, Ordering::Relaxed); - let res = tokio::select! { - r = requester => {r} - r = peer_connection.manage_peer(rx) => {r} - }; - - handler.state.peer_semaphore.add_permits(1); - - match res { - // We disconnected the peer ourselves as we don't need it - Ok(()) => { - handler.on_peer_died(None); - } - Err(e) => { - debug!("error managing peer: {:#}", e); - handler.on_peer_died(Some(e)); - } - } - Ok::<_, anyhow::Error>(()) - } - - pub async fn task_peer_adder( - self: Arc, - mut peer_queue_rx: UnboundedReceiver, - spawner: BlockingSpawner, - ) -> anyhow::Result<()> { - let state = self; - loop { - let addr = peer_queue_rx.recv().await.unwrap(); - if state.is_finished() { - debug!("ignoring peer {} as we are finished", addr); - state.peers.mark_peer_not_needed(addr); - continue; - } - - let permit = state.peer_semaphore.acquire().await.unwrap(); - permit.forget(); - spawn( - span!(parent: None, Level::ERROR, "manage_peer", peer = addr.to_string()), - state.clone().task_manage_peer(addr, spawner), - ); - } - } - - pub fn info(&self) -> &TorrentMetaV1Info { - &self.info - } - pub fn info_hash(&self) -> Id20 { - self.info_hash - } - pub fn peer_id(&self) -> Id20 { - self.peer_id - } - pub fn file_ops(&self) -> FileOps<'_, Sha1> { - FileOps::new(&self.info, &self.files, &self.lengths) - } - pub fn initially_needed(&self) -> u64 { - self.needed_bytes - } - pub fn lock_read( - &self, - reason: &'static str, - ) -> TimedExistence> { - TimedExistence::new(timeit(reason, || self.locked.read()), reason) - } - pub fn lock_write( - &self, - reason: &'static str, - ) -> TimedExistence> { - TimedExistence::new(timeit(reason, || self.locked.write()), reason) - } - - fn get_next_needed_piece(&self, peer_handle: PeerHandle) -> Option { - self.peers - .with_live_mut(peer_handle, "l(get_next_needed_piece)", |live| { - let g = self.lock_read("g(get_next_needed_piece)"); - let bf = &live.bitfield; - for n in g.chunks.iter_needed_pieces() { - if bf.get(n).map(|v| *v) == Some(true) { - // in theory it should be safe without validation, but whatever. - return self.lengths.validate_piece_index(n as u32); - } - } - None - })? - } - - fn am_i_interested_in_peer(&self, handle: PeerHandle) -> bool { - self.get_next_needed_piece(handle).is_some() - } - - fn set_peer_live(&self, handle: PeerHandle, h: Handshake) { - let result = self.peers.with_peer_mut(handle, "set_peer_live", |p| { - p.state - .connecting_to_live(Id20(h.peer_id), &self.peers.stats) - .is_some() - }); - match result { - Some(true) => { - debug!("set peer to live") - } - Some(false) => debug!("can't set peer live, it was in wrong state"), - None => debug!("can't set peer live, it disappeared"), - } - } - - pub fn get_uploaded_bytes(&self) -> u64 { - self.stats.uploaded_bytes.load(Ordering::Relaxed) - } - pub fn get_downloaded_bytes(&self) -> u64 { - self.stats - .downloaded_and_checked_bytes - .load(Ordering::Acquire) - } - - pub fn is_finished(&self) -> bool { - self.get_left_to_download_bytes() == 0 - } - - pub fn get_left_to_download_bytes(&self) -> u64 { - self.needed_bytes - self.get_downloaded_bytes() - } - - fn maybe_transmit_haves(&self, index: ValidPieceIndex) { - let mut futures = Vec::new(); - - for pe in self.peers.states.iter() { - match &pe.value().state.get() { - PeerState::Live(live) => { - if !live.peer_interested { - continue; - } - - if live - .bitfield - .get(index.get() as usize) - .map(|v| *v) - .unwrap_or(false) - { - continue; - } - - let tx = live.tx.downgrade(); - futures.push(async move { - if let Some(tx) = tx.upgrade() { - if tx - .send(WriterRequest::Message(Message::Have(index.get()))) - .is_err() - { - // whatever - } - } - }); - } - _ => continue, - } - } - - if futures.is_empty() { - trace!("no peers to transmit Have={} to, saving some work", index); - return; - } - - let mut unordered: FuturesUnordered<_> = futures.into_iter().collect(); - spawn( - span!( - Level::ERROR, - "transmit_haves", - piece = index.get(), - count = unordered.len() - ), - async move { - while unordered.next().await.is_some() {} - Ok(()) - }, - ); - } - - pub fn add_peer_if_not_seen(self: &Arc, addr: SocketAddr) -> bool { - match self.peers.add_if_not_seen(addr) { - Some(handle) => handle, - None => return false, - }; - - self.peer_queue_tx.send(addr).unwrap(); - true - } - - pub fn stats_snapshot(&self) -> StatsSnapshot { - use Ordering::*; - let downloaded_bytes = self.stats.downloaded_and_checked_bytes.load(Relaxed); - let remaining = self.needed_bytes - downloaded_bytes; - StatsSnapshot { - have_bytes: self.stats.have_bytes.load(Relaxed), - downloaded_and_checked_bytes: downloaded_bytes, - downloaded_and_checked_pieces: self.stats.downloaded_and_checked_pieces.load(Relaxed), - fetched_bytes: self.stats.fetched_bytes.load(Relaxed), - uploaded_bytes: self.stats.uploaded_bytes.load(Relaxed), - total_bytes: self.have_plus_needed_bytes, - time: Instant::now(), - initially_needed_bytes: self.needed_bytes, - remaining_bytes: remaining, - total_piece_download_ms: self.stats.total_piece_download_ms.load(Relaxed), - peer_stats: self.peers.stats(), - } - } - - pub fn per_peer_stats_snapshot(&self, filter: PeerStatsFilter) -> PeerStatsSnapshot { - PeerStatsSnapshot { - peers: self - .peers - .states - .iter() - .filter(|e| filter.state.matches(e.value().state.get())) - .map(|e| (e.key().to_string(), e.value().into())) - .collect(), - } - } - - pub async fn wait_until_completed(&self) { - if self.is_finished() { - return; - } - self.finished_notify.notified().await; - } -} - -struct PeerHandlerLocked { - pub i_am_choked: bool, - - // This is used to only request a piece from a peer once when stealing from others. - // So that you don't steal then re-steal the same piece in a loop. - pub previously_requested_pieces: BF, -} - -// All peer state that would never be used by other actors should pe put here. -// This state tracks a live peer. -struct PeerHandler { - state: Arc, - counters: Arc, - // Semantically, we don't need an RwLock here, as this is only requested from - // one future (requester + manage_peer). - // - // However as PeerConnectionHandler takes &self everywhere, we need shared mutability. - // RefCell would do, but tokio is unhappy when we use it. - locked: RwLock, - - // This is used to unpause chunk requester once the bitfield - // is received. - on_bitfield_notify: Notify, - - // This is used to unpause after we were choked. - unchoke_notify: Notify, - - // This is used to limit the number of chunk requests we send to a peer at a time. - requests_sem: Semaphore, - - addr: SocketAddr, - spawner: BlockingSpawner, - - tx: PeerTx, -} - -impl<'a> PeerConnectionHandler for &'a PeerHandler { - fn on_connected(&self, connection_time: Duration) { - self.counters.connections.fetch_add(1, Ordering::Relaxed); - self.counters - .total_time_connecting_ms - .fetch_add(connection_time.as_millis() as u64, Ordering::Relaxed); - } - fn on_received_message(&self, message: Message>) -> anyhow::Result<()> { - match message { - Message::Request(request) => { - self.on_download_request(request) - .context("on_download_request")?; - } - Message::Bitfield(b) => self - .on_bitfield(b.clone_to_owned()) - .context("on_bitfield")?, - Message::Choke => self.on_i_am_choked(), - Message::Unchoke => self.on_i_am_unchoked(), - Message::Interested => self.on_peer_interested(), - Message::Piece(piece) => self.on_received_piece(piece).context("on_received_piece")?, - Message::KeepAlive => { - debug!("keepalive received"); - } - Message::Have(h) => self.on_have(h), - Message::NotInterested => { - info!("received \"not interested\", but we don't care yet") - } - message => { - warn!("received unsupported message {:?}, ignoring", message); - } - }; - Ok(()) - } - - fn get_have_bytes(&self) -> u64 { - self.state.stats.have_bytes.load(Ordering::Relaxed) - } - - fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec) -> Option { - let g = self.state.lock_read("serialize_bitfield_message_to_buf"); - let msg = Message::Bitfield(ByteBuf(g.chunks.get_have_pieces().as_raw_slice())); - let len = msg.serialize(buf, None).unwrap(); - debug!("sending: {:?}, length={}", &msg, len); - Some(len) - } - - fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> { - self.state.set_peer_live(self.addr, handshake); - Ok(()) - } - - fn on_uploaded_bytes(&self, bytes: u32) { - self.state - .stats - .uploaded_bytes - .fetch_add(bytes as u64, Ordering::Relaxed); - } - - fn read_chunk(&self, chunk: &ChunkInfo, buf: &mut [u8]) -> anyhow::Result<()> { - self.state.file_ops().read_chunk(self.addr, chunk, buf) - } - - fn on_extended_handshake(&self, _: &ExtendedHandshake) -> anyhow::Result<()> { - Ok(()) - } -} - -impl PeerHandler { - fn on_peer_died(self, error: Option) { - let peers = &self.state.peers; - let pstats = &peers.stats; - let handle = self.addr; - let mut pe = match peers.states.get_mut(&handle) { - Some(peer) => TimedExistence::new(peer, "on_peer_died"), - None => { - warn!("bug: peer not found in table. Forgetting it forever"); - return; - } - }; - let prev = pe.value_mut().state.take(pstats); - - match prev { - PeerState::Connecting(_) => {} - PeerState::Live(live) => { - let mut g = self.state.lock_write("mark_chunk_requests_canceled"); - for req in live.inflight_requests { - debug!( - "peer dead, marking chunk request cancelled, index={}, chunk={}", - req.piece.get(), - req.chunk - ); - g.chunks.mark_chunk_request_cancelled(req.piece, req.chunk); - } - } - PeerState::NotNeeded => { - // Restore it as std::mem::take() replaced it above. - pe.value_mut().state.set(PeerState::NotNeeded, pstats); - return; - } - s @ PeerState::Queued | s @ PeerState::Dead => { - warn!("bug: peer was in a wrong state {s:?}, ignoring it forever"); - // Prevent deadlocks. - drop(pe); - self.state.peers.drop_peer(handle); - return; - } - }; - - let _error = match error { - Some(e) => e, - None => { - debug!("peer died without errors, not re-queueing"); - pe.value_mut().state.set(PeerState::NotNeeded, pstats); - return; - } - }; - - self.counters.errors.fetch_add(1, Ordering::Relaxed); - - if self.state.is_finished() { - debug!("torrent finished, not re-queueing"); - pe.value_mut().state.set(PeerState::NotNeeded, pstats); - return; - } - - pe.value_mut().state.set(PeerState::Dead, pstats); - - let backoff = pe.value_mut().stats.backoff.next_backoff(); - - // Prevent deadlocks. - drop(pe); - - if let Some(dur) = backoff { - spawn( - span!( - parent: None, - Level::ERROR, - "wait_for_peer", - peer = handle.to_string(), - duration = format!("{dur:?}") - ), - async move { - tokio::time::sleep(dur).await; - self.state - .peers - .with_peer_mut(handle, "dead_to_queued", |peer| { - match peer.state.get() { - PeerState::Dead => { - peer.state.set(PeerState::Queued, &self.state.peers.stats) - } - other => bail!( - "peer is in unexpected state: {}. Expected dead", - other.name() - ), - }; - Ok(()) - }) - .context("bug: peer disappeared")??; - self.state.peer_queue_tx.send(handle)?; - Ok::<_, anyhow::Error>(()) - }, - ); - } else { - debug!("dropping peer, backoff exhausted"); - self.state.peers.drop_peer(handle); - } - } - - fn reserve_next_needed_piece(&self) -> Option { - // TODO: locking one inside the other in different order results in deadlocks. - self.state - .peers - .with_live_mut(self.addr, "reserve_next_needed_piece", |live| { - if self.locked.read().i_am_choked { - debug!("we are choked, can't reserve next piece"); - return None; - } - let mut g = self.state.lock_write("reserve_next_needed_piece"); - - let n = { - let mut n_opt = None; - let bf = &live.bitfield; - for n in g.chunks.iter_needed_pieces() { - if bf.get(n).map(|v| *v) == Some(true) { - n_opt = Some(n); - break; - } - } - - self.state.lengths.validate_piece_index(n_opt? as u32)? - }; - g.inflight_pieces.insert( - n, - InflightPiece { - peer: self.addr, - started: Instant::now(), - }, - ); - g.chunks.reserve_needed_piece(n); - Some(n) - }) - .flatten() - } - - fn try_steal_old_slow_piece(&self, threshold: f64) -> Option { - let total = self - .state - .stats - .downloaded_and_checked_pieces - .load(Ordering::Acquire); - - // heuristic for not enough precision in average time - if total < 20 { - return None; - } - let avg_time = self.state.stats.average_piece_download_time()?; - - let mut g = self.state.lock_write("try_steal_old_slow_piece"); - let (idx, elapsed, piece_req) = g - .inflight_pieces - .iter_mut() - // don't steal from myself - .filter(|(_, r)| r.peer != self.addr) - .map(|(p, r)| (p, r.started.elapsed(), r)) - .max_by_key(|(_, e, _)| *e)?; - - // heuristic for "too slow peer" - if elapsed.as_secs_f64() > avg_time.as_secs_f64() * threshold { - debug!( - "will steal piece {} from {}: elapsed time {:?}, avg piece time: {:?}", - idx, piece_req.peer, elapsed, avg_time - ); - piece_req.peer = self.addr; - piece_req.started = Instant::now(); - return Some(*idx); - } - None - } - - fn on_download_request(&self, request: Request) -> anyhow::Result<()> { - let piece_index = match self.state.lengths.validate_piece_index(request.index) { - Some(p) => p, - None => { - anyhow::bail!( - "received {:?}, but it is not a valid chunk request (piece index is invalid). Ignoring.", - request - ); - } - }; - let chunk_info = match self.state.lengths.chunk_info_from_received_data( - piece_index, - request.begin, - request.length, - ) { - Some(d) => d, - None => { - anyhow::bail!( - "received {:?}, but it is not a valid chunk request (chunk data is invalid). Ignoring.", - request - ); - } - }; - - if !self - .state - .lock_read("is_chunk_ready_to_upload") - .chunks - .is_chunk_ready_to_upload(&chunk_info) - { - anyhow::bail!( - "got request for a chunk that is not ready to upload. chunk {:?}", - &chunk_info - ); - } - - // TODO: this is not super efficient as it does copying multiple times. - // Theoretically, this could be done in the sending code, so that it reads straight into - // the send buffer. - let request = WriterRequest::ReadChunkRequest(chunk_info); - debug!("sending {:?}", &request); - Ok::<_, anyhow::Error>(self.tx.send(request)?) - } - - fn on_have(&self, have: u32) { - self.state - .peers - .with_live_mut(self.addr, "on_have", |live| { - // If bitfield wasn't allocated yet, let's do it. Some clients send haves before bitfield. - if live.bitfield.is_empty() { - live.bitfield = - BF::from_vec(vec![0; self.state.lengths.piece_bitfield_bytes()]); - } - match live.bitfield.get_mut(have as usize) { - Some(mut v) => *v = true, - None => { - warn!("received have {} out of range", have); - return; - } - }; - debug!("updated bitfield with have={}", have); - }); - } - - fn on_bitfield(&self, bitfield: ByteString) -> anyhow::Result<()> { - if bitfield.len() != self.state.lengths.piece_bitfield_bytes() { - anyhow::bail!( - "dropping peer as its bitfield has unexpected size. Got {}, expected {}", - bitfield.len(), - self.state.lengths.piece_bitfield_bytes(), - ); - } - self.locked.write().previously_requested_pieces = BF::from_vec(vec![0; bitfield.len()]); - self.state - .peers - .update_bitfield_from_vec(self.addr, bitfield.0); - - if !self.state.am_i_interested_in_peer(self.addr) { - self.tx - .send(WriterRequest::Message(MessageOwned::Unchoke))?; - self.tx - .send(WriterRequest::Message(MessageOwned::NotInterested))?; - if self.state.is_finished() { - self.tx.send(WriterRequest::Disconnect)?; - } - return Ok(()); - } - - self.on_bitfield_notify.notify_waiters(); - Ok(()) - } - - async fn task_peer_chunk_requester(&self, handle: PeerHandle) -> anyhow::Result<()> { - self.on_bitfield_notify.notified().await; - self.tx.send_many([ - WriterRequest::Message(MessageOwned::Unchoke), - WriterRequest::Message(MessageOwned::Interested), - ])?; - - #[allow(unused_must_use)] - { - timeout(Duration::from_secs(60), self.unchoke_notify.notified()).await; - } - - loop { - if self.locked.read().i_am_choked { - debug!("we are choked, can't reserve next piece"); - #[allow(unused_must_use)] - { - timeout(Duration::from_secs(60), self.unchoke_notify.notified()).await; - } - continue; - } - - if self.state.is_finished() { - debug!("nothing left to download, looping forever until manage_peer quits"); - loop { - tokio::time::sleep(Duration::from_secs(86400)).await; - } - } - - // Try steal a pice from a very slow peer first. Otherwise we might wait too long - // to download early pieces. - // Then try get the next one in queue. - // Afterwards means we are close to completion, try stealing more aggressively. - let next = match self - .try_steal_old_slow_piece(10.) - .or_else(|| self.reserve_next_needed_piece()) - .or_else(|| self.try_steal_old_slow_piece(2.)) - { - Some(next) => next, - None => { - debug!("no pieces to request"); - tokio::time::sleep(Duration::from_secs(10)).await; - continue; - } - }; - - self.locked - .write() - .previously_requested_pieces - .set(next.get() as usize, true); - - for chunk in self.state.lengths.iter_chunk_infos(next) { - let request = Request { - index: next.get(), - begin: chunk.offset, - length: chunk.size, - }; - - match self - .state - .peers - .with_live_mut(handle, "add chunk request", |live| { - live.inflight_requests.insert(InflightRequest::from(&chunk)) - }) { - Some(true) => {} - Some(false) => { - // This request was already in-flight for this peer for this chunk. - // This might happen in theory, but not very likely. - // - // Example: - // someone stole a piece from us, and then died, the piece became "needed" again, and we reserved it - // all before the piece request was processed by us. - warn!("we already requested {:?} previously", chunk); - continue; - } - // peer died - None => return Ok(()), - }; - - loop { - match timeout(Duration::from_secs(10), self.requests_sem.acquire()).await { - Ok(acq) => break acq?.forget(), - Err(_) => continue, - }; - } - - if self - .tx - .send(WriterRequest::Message(MessageOwned::Request(request))) - .is_err() - { - return Ok(()); - } - } - } - } - - fn on_i_am_choked(&self) { - self.locked.write().i_am_choked = true; - } - - fn on_peer_interested(&self) { - debug!("peer is interested"); - self.state.peers.mark_peer_interested(self.addr, true); - } - - fn reopen_read_only(&self) -> anyhow::Result<()> { - fn dummy_file() -> anyhow::Result { - #[cfg(target_os = "windows")] - const DEVNULL: &str = "NUL"; - #[cfg(not(target_os = "windows"))] - const DEVNULL: &str = "/dev/null"; - - std::fs::OpenOptions::new() - .read(true) - .open(DEVNULL) - .with_context(|| format!("error opening {}", DEVNULL)) - } - - // Lock exclusive just in case to ensure in-flight operations finish.?? - let _guard = self.state.lock_write("reopen_read_only"); - - for (file, filename) in self.state.files.iter().zip(self.state.filenames.iter()) { - let mut g = file.lock(); - // this should close the original file - // putting in a block just in case to guarantee drop. - { - *g = dummy_file()?; - } - *g = std::fs::OpenOptions::new() - .read(true) - .open(filename) - .with_context(|| format!("error re-opening {:?} readonly", filename))?; - debug!("reopened {:?} read-only", filename); - } - info!("reopened all torrent files in read-only mode"); - Ok(()) - } - - fn on_i_am_unchoked(&self) { - debug!("we are unchoked"); - self.locked.write().i_am_choked = false; - self.unchoke_notify.notify_waiters(); - self.requests_sem.add_permits(16); - } - - fn on_received_piece(&self, piece: Piece) -> anyhow::Result<()> { - let chunk_info = match self.state.lengths.chunk_info_from_received_piece( - piece.index, - piece.begin, - piece.block.len() as u32, - ) { - Some(i) => i, - None => { - anyhow::bail!("peer sent us an invalid piece {:?}", &piece,); - } - }; - - self.requests_sem.add_permits(1); - - // Peer chunk/byte counters. - self.counters - .fetched_bytes - .fetch_add(piece.block.len() as u64, Ordering::Relaxed); - self.counters.fetched_chunks.fetch_add(1, Ordering::Relaxed); - - // Global chunk/byte counters. - self.state - .stats - .fetched_bytes - .fetch_add(piece.block.len() as u64, Ordering::Relaxed); - - self.state - .peers - .with_live_mut(self.addr, "inflight_requests.remove", |h| { - if !h - .inflight_requests - .remove(&InflightRequest::from(&chunk_info)) - { - anyhow::bail!( - "peer sent us a piece we did not ask. Requested pieces: {:?}. Got: {:?}", - &h.inflight_requests, - &piece, - ); - } - Ok(()) - }) - .context("peer not found")??; - - let full_piece_download_time = { - let mut g = self.state.lock_write("mark_chunk_downloaded"); - - match g.inflight_pieces.get(&chunk_info.piece_index) { - Some(InflightPiece { peer, .. }) if *peer == self.addr => {} - Some(InflightPiece { peer, .. }) => { - debug!( - "in-flight piece {} was stolen by {}, ignoring", - chunk_info.piece_index, peer - ); - return Ok(()); - } - None => { - debug!( - "in-flight piece {} not found. it was probably completed by someone else", - chunk_info.piece_index - ); - return Ok(()); - } - }; - - match g.chunks.mark_chunk_downloaded(&piece) { - Some(ChunkMarkingResult::Completed) => { - debug!("piece={} done, will write and checksum", piece.index,); - // This will prevent others from stealing it. - { - let piece = chunk_info.piece_index; - g.inflight_pieces.remove(&piece) - } - .map(|t| t.started.elapsed()) - } - Some(ChunkMarkingResult::PreviouslyCompleted) => { - // TODO: we might need to send cancellations here. - debug!("piece={} was done by someone else, ignoring", piece.index,); - return Ok(()); - } - Some(ChunkMarkingResult::NotCompleted) => None, - None => { - anyhow::bail!( - "bogus data received: {:?}, cannot map this to a chunk, dropping peer", - piece - ); - } - } - }; - - // By this time we reach here, no other peer can for this piece. All others, even if they steal pieces would - // have fallen off above in one of the defensive checks. - - self.spawner - .spawn_block_in_place(move || { - let index = piece.index; - - // TODO: in theory we should unmark the piece as downloaded here. But if there was a disk error, what - // should we really do? If we unmark it, it will get requested forever... - // - // So let's just unwrap and abort. - match self - .state - .file_ops() - .write_chunk(self.addr, &piece, &chunk_info) - { - Ok(()) => {} - Err(e) => { - error!("FATAL: error writing chunk to disk: {:?}", e); - panic!("{:?}", e); - } - } - - let full_piece_download_time = match full_piece_download_time { - Some(t) => t, - None => return Ok(()), - }; - - match self - .state - .file_ops() - .check_piece(self.addr, chunk_info.piece_index, &chunk_info) - .with_context(|| format!("error checking piece={index}"))? - { - true => { - { - let mut g = self.state.lock_write("mark_piece_downloaded"); - g.chunks.mark_piece_downloaded(chunk_info.piece_index); - } - - // Global piece counters. - let piece_len = - self.state.lengths.piece_length(chunk_info.piece_index) as u64; - self.state - .stats - .downloaded_and_checked_bytes - // This counter is used to compute "is_finished", so using - // stronger ordering. - .fetch_add(piece_len, Ordering::Release); - self.state - .stats - .downloaded_and_checked_pieces - // This counter is used to compute "is_finished", so using - // stronger ordering. - .fetch_add(1, Ordering::Release); - self.state - .stats - .have_bytes - .fetch_add(piece_len, Ordering::Relaxed); - self.state.stats.total_piece_download_ms.fetch_add( - full_piece_download_time.as_millis() as u64, - Ordering::Release, - ); - - // Per-peer piece counters. - self.counters - .downloaded_and_checked_pieces - .fetch_add(1, Ordering::Relaxed); - self.counters - .downloaded_and_checked_bytes - .fetch_add(piece_len, Ordering::Relaxed); - - self.state.peers.reset_peer_backoff(self.addr); - - debug!("piece={} successfully downloaded and verified", index); - - if self.state.is_finished() { - info!("torrent finished downloading"); - self.state.finished_notify.notify_waiters(); - self.disconnect_all_peers_that_have_full_torrent(); - self.reopen_read_only()?; - } - - self.state.maybe_transmit_haves(chunk_info.piece_index); - } - false => { - warn!("checksum for piece={} did not validate", index,); - self.state - .lock_write("mark_piece_broken") - .chunks - .mark_piece_broken(chunk_info.piece_index); - } - }; - Ok::<_, anyhow::Error>(()) - }) - .with_context(|| format!("error processing received chunk {chunk_info:?}"))?; - Ok(()) - } - - fn disconnect_all_peers_that_have_full_torrent(&self) { - for mut pe in self.state.peers.states.iter_mut() { - if let PeerState::Live(l) = pe.value().state.get() { - if l.has_full_torrent(self.state.lengths.total_pieces() as usize) { - let prev = pe.value_mut().state.to_not_needed(&self.state.peers.stats); - let _ = prev - .take_live_no_counters() - .unwrap() - .tx - .send(WriterRequest::Disconnect); - } - } - } - } -} +pub use live::*; From edaf3e099707368062b57c186bbc4704189069e7 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Thu, 23 Nov 2023 16:27:55 +0000 Subject: [PATCH 03/38] split up librqbit torrent state into smaller files --- crates/librqbit/src/http_api.rs | 10 +- crates/librqbit/src/lib.rs | 1 - crates/librqbit/src/torrent_state/live/mod.rs | 294 ++---------------- .../live/peer/mod.rs} | 160 +--------- .../torrent_state/live/peer/stats/atomic.rs | 41 +++ .../src/torrent_state/live/peer/stats/mod.rs | 2 + .../torrent_state/live/peer/stats/snapshot.rs | 74 +++++ .../src/torrent_state/live/peers/mod.rs | 114 +++++++ .../torrent_state/live/peers/stats/atomic.rs | 43 +++ .../src/torrent_state/live/peers/stats/mod.rs | 2 + .../live/peers/stats/snapshot.rs | 29 ++ .../src/torrent_state/live/stats/atomic.rs | 25 ++ .../src/torrent_state/live/stats/mod.rs | 2 + .../src/torrent_state/live/stats/snapshot.rs | 32 ++ crates/librqbit/src/torrent_state/mod.rs | 2 + crates/librqbit/src/torrent_state/utils.rs | 108 +++++++ crates/rqbit/src/main.rs | 3 +- 17 files changed, 502 insertions(+), 440 deletions(-) rename crates/librqbit/src/{peer_state.rs => torrent_state/live/peer/mod.rs} (54%) create mode 100644 crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs create mode 100644 crates/librqbit/src/torrent_state/live/peer/stats/mod.rs create mode 100644 crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs create mode 100644 crates/librqbit/src/torrent_state/live/peers/mod.rs create mode 100644 crates/librqbit/src/torrent_state/live/peers/stats/atomic.rs create mode 100644 crates/librqbit/src/torrent_state/live/peers/stats/mod.rs create mode 100644 crates/librqbit/src/torrent_state/live/peers/stats/snapshot.rs create mode 100644 crates/librqbit/src/torrent_state/live/stats/atomic.rs create mode 100644 crates/librqbit/src/torrent_state/live/stats/mod.rs create mode 100644 crates/librqbit/src/torrent_state/live/stats/snapshot.rs create mode 100644 crates/librqbit/src/torrent_state/utils.rs diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 43077fe..11395d8 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -19,12 +19,12 @@ use tracing::{info, warn}; use axum::Router; use crate::http_api_error::{ApiError, ApiErrorExt}; -use crate::peer_state::PeerStatsFilter; use crate::session::{ AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, }; use crate::torrent_manager::TorrentManagerHandle; -use crate::torrent_state::StatsSnapshot; +use crate::torrent_state::peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot}; +use crate::torrent_state::stats::snapshot::StatsSnapshot; // Public API #[derive(Clone)] @@ -386,11 +386,7 @@ impl ApiInternal { make_torrent_details(&info_hash, handle.torrent_state().info(), only_files) } - fn api_peer_stats( - &self, - idx: usize, - filter: PeerStatsFilter, - ) -> Result { + fn api_peer_stats(&self, idx: usize, filter: PeerStatsFilter) -> Result { let handle = self.mgr_handle(idx)?; Ok(handle.torrent_state().per_peer_stats_snapshot(filter)) } diff --git a/crates/librqbit/src/lib.rs b/crates/librqbit/src/lib.rs index 097d45e..78d44aa 100644 --- a/crates/librqbit/src/lib.rs +++ b/crates/librqbit/src/lib.rs @@ -6,7 +6,6 @@ pub mod http_api_client; mod http_api_error; pub mod peer_connection; pub mod peer_info_reader; -pub mod peer_state; pub mod session; pub mod spawn_utils; pub mod torrent_manager; diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index c0a84d3..fa49040 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -39,6 +39,10 @@ // > so don't lock them both at the same time at all, or at the worst lock them in the // > same order (peers one first, then the global one). +pub mod peer; +pub mod peers; +pub mod stats; + use std::{ collections::HashMap, fs::File, @@ -55,7 +59,6 @@ use anyhow::{bail, Context}; use backoff::backoff::Backoff; use buffers::{ByteBuf, ByteString}; use clone_to_owned::CloneToOwned; -use dashmap::DashMap; use futures::{stream::FuturesUnordered, StreamExt}; use librqbit_core::{ id20::Id20, @@ -66,7 +69,6 @@ use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; use peer_binary_protocol::{ extended::handshake::ExtendedHandshake, Handshake, Message, MessageOwned, Piece, Request, }; -use serde::Serialize; use sha1w::Sha1; use tokio::{ sync::{ @@ -83,140 +85,29 @@ use crate::{ peer_connection::{ PeerConnection, PeerConnectionHandler, PeerConnectionOptions, WriterRequest, }, - peer_state::{ - atomic_inc, AggregatePeerStatsAtomic, InflightRequest, LivePeerState, Peer, PeerCounters, - PeerRx, PeerState, PeerStatsFilter, PeerStatsSnapshot, PeerTx, SendMany, - }, spawn_utils::{spawn, BlockingSpawner}, type_aliases::{PeerHandle, BF}, }; +use self::{ + peer::{ + stats::{ + atomic::PeerCounters as AtomicPeerCounters, + snapshot::{PeerStatsFilter, PeerStatsSnapshot}, + }, + InflightRequest, PeerState, PeerTx, SendMany, + }, + peers::PeerStates, + stats::{atomic::AtomicStats, snapshot::StatsSnapshot}, +}; + +use super::utils::{timeit, TimedExistence}; + pub struct InflightPiece { pub peer: PeerHandle, pub started: Instant, } -#[derive(Default)] -pub struct PeerStates { - stats: AggregatePeerStatsAtomic, - states: DashMap, -} - -#[derive(Debug, Default, Serialize, PartialEq, Eq)] -pub struct AggregatePeerStats { - pub queued: usize, - pub connecting: usize, - pub live: usize, - pub seen: usize, - pub dead: usize, - pub not_needed: usize, -} - -impl<'a> From<&'a AggregatePeerStatsAtomic> for AggregatePeerStats { - fn from(s: &'a AggregatePeerStatsAtomic) -> Self { - let ordering = Ordering::Relaxed; - Self { - queued: s.queued.load(ordering) as usize, - connecting: s.connecting.load(ordering) as usize, - live: s.live.load(ordering) as usize, - seen: s.seen.load(ordering) as usize, - dead: s.dead.load(ordering) as usize, - not_needed: s.not_needed.load(ordering) as usize, - } - } -} - -impl PeerStates { - pub fn stats(&self) -> AggregatePeerStats { - AggregatePeerStats::from(&self.stats) - } - - pub fn add_if_not_seen(&self, addr: SocketAddr) -> Option { - use dashmap::mapref::entry::Entry; - match self.states.entry(addr) { - Entry::Occupied(_) => None, - Entry::Vacant(vac) => { - vac.insert(Default::default()); - atomic_inc(&self.stats.queued); - atomic_inc(&self.stats.seen); - Some(addr) - } - } - } - pub fn with_peer(&self, addr: PeerHandle, f: impl FnOnce(&Peer) -> R) -> Option { - self.states.get(&addr).map(|e| f(e.value())) - } - - pub fn with_peer_mut( - &self, - addr: PeerHandle, - reason: &'static str, - f: impl FnOnce(&mut Peer) -> R, - ) -> Option { - timeit(reason, || self.states.get_mut(&addr)) - .map(|e| f(TimedExistence::new(e, reason).value_mut())) - } - pub fn with_live(&self, addr: PeerHandle, f: impl FnOnce(&LivePeerState) -> R) -> Option { - self.states - .get(&addr) - .and_then(|e| match &e.value().state.get() { - PeerState::Live(l) => Some(f(l)), - _ => None, - }) - } - pub fn with_live_mut( - &self, - addr: PeerHandle, - reason: &'static str, - f: impl FnOnce(&mut LivePeerState) -> R, - ) -> Option { - self.with_peer_mut(addr, reason, |peer| peer.state.get_live_mut().map(f)) - .flatten() - } - - pub fn drop_peer(&self, handle: PeerHandle) -> Option { - let p = self.states.remove(&handle).map(|r| r.1)?; - self.stats.dec(p.state.get()); - Some(p) - } - - pub fn mark_peer_interested(&self, handle: PeerHandle, is_interested: bool) -> Option { - self.with_live_mut(handle, "mark_peer_interested", |live| { - let prev = live.peer_interested; - live.peer_interested = is_interested; - prev - }) - } - pub fn update_bitfield_from_vec(&self, handle: PeerHandle, bitfield: Vec) -> Option<()> { - self.with_live_mut(handle, "update_bitfield_from_vec", |live| { - live.bitfield = BF::from_vec(bitfield); - }) - } - pub fn mark_peer_connecting(&self, h: PeerHandle) -> anyhow::Result<(PeerRx, PeerTx)> { - let rx = self - .with_peer_mut(h, "mark_peer_connecting", |peer| { - peer.state - .queued_to_connecting(&self.stats) - .context("invalid peer state") - }) - .context("peer not found in states")??; - Ok(rx) - } - - fn reset_peer_backoff(&self, handle: PeerHandle) { - self.with_peer_mut(handle, "reset_peer_backoff", |p| { - p.stats.backoff.reset(); - }); - } - - fn mark_peer_not_needed(&self, handle: PeerHandle) -> Option { - let prev = self.with_peer_mut(handle, "mark_peer_not_needed", |peer| { - peer.state.to_not_needed(&self.stats) - })?; - Some(prev) - } -} - pub struct TorrentStateLocked { // What chunks we have and need. pub chunks: ChunkTracker, @@ -226,54 +117,6 @@ pub struct TorrentStateLocked { pub inflight_pieces: HashMap, } -#[derive(Default, Debug)] -struct AtomicStats { - have_bytes: AtomicU64, - downloaded_and_checked_bytes: AtomicU64, - downloaded_and_checked_pieces: AtomicU64, - uploaded_bytes: AtomicU64, - fetched_bytes: AtomicU64, - total_piece_download_ms: AtomicU64, -} - -impl AtomicStats { - fn average_piece_download_time(&self) -> Option { - let d = self.downloaded_and_checked_pieces.load(Ordering::Acquire); - let t = self.total_piece_download_ms.load(Ordering::Acquire); - if d == 0 { - return None; - } - Some(Duration::from_secs_f64(t as f64 / d as f64 / 1000f64)) - } -} - -#[derive(Debug, Serialize)] -pub struct StatsSnapshot { - pub have_bytes: u64, - pub downloaded_and_checked_bytes: u64, - pub downloaded_and_checked_pieces: u64, - pub fetched_bytes: u64, - pub uploaded_bytes: u64, - pub initially_needed_bytes: u64, - pub remaining_bytes: u64, - pub total_bytes: u64, - #[serde(skip)] - pub time: Instant, - pub total_piece_download_ms: u64, - pub peer_stats: AggregatePeerStats, -} - -impl StatsSnapshot { - pub fn average_piece_download_time(&self) -> Option { - let d = self.downloaded_and_checked_pieces; - let t = self.total_piece_download_ms; - if d == 0 { - return None; - } - Some(Duration::from_secs_f64(t as f64 / d as f64 / 1000f64)) - } -} - #[derive(Default)] pub struct TorrentStateOptions { pub peer_connect_timeout: Option, @@ -303,105 +146,6 @@ pub struct TorrentState { finished_notify: Notify, } -// Used during debugging to see if some locks take too long. -#[cfg(not(feature = "timed_existence"))] -mod timed_existence { - use std::ops::{Deref, DerefMut}; - - pub struct TimedExistence(T); - - impl TimedExistence { - #[inline(always)] - pub fn new(object: T, _reason: &'static str) -> Self { - Self(object) - } - } - - impl Deref for TimedExistence { - type Target = T; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl DerefMut for TimedExistence { - #[inline(always)] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - #[inline(always)] - pub fn timeit(_n: impl std::fmt::Display, f: impl FnOnce() -> R) -> R { - f() - } -} - -#[cfg(feature = "timed_existence")] -mod timed_existence { - use std::ops::{Deref, DerefMut}; - use std::time::{Duration, Instant}; - use tracing::warn; - - const MAX: Duration = Duration::from_millis(1); - - // Prints if the object exists for too long. - // This is used to track long-lived locks for debugging. - pub struct TimedExistence { - object: T, - reason: &'static str, - started: Instant, - } - - impl TimedExistence { - pub fn new(object: T, reason: &'static str) -> Self { - Self { - object, - reason, - started: Instant::now(), - } - } - } - - impl Drop for TimedExistence { - fn drop(&mut self) { - let elapsed = self.started.elapsed(); - let reason = self.reason; - if elapsed > MAX { - warn!("elapsed on lock {reason:?}: {elapsed:?}") - } - } - } - - impl Deref for TimedExistence { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.object - } - } - - impl DerefMut for TimedExistence { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.object - } - } - - pub fn timeit(name: impl std::fmt::Display, f: impl FnOnce() -> R) -> R { - let now = Instant::now(); - let r = f(); - let elapsed = now.elapsed(); - if elapsed > MAX { - warn!("elapsed on \"{name:}\": {elapsed:?}") - } - r - } -} - -pub use timed_existence::{timeit, TimedExistence}; - impl TorrentState { #[allow(clippy::too_many_arguments)] pub fn new( @@ -734,7 +478,7 @@ struct PeerHandlerLocked { // This state tracks a live peer. struct PeerHandler { state: Arc, - counters: Arc, + counters: Arc, // Semantically, we don't need an RwLock here, as this is only requested from // one future (requester + manage_peer). // diff --git a/crates/librqbit/src/peer_state.rs b/crates/librqbit/src/torrent_state/live/peer/mod.rs similarity index 54% rename from crates/librqbit/src/peer_state.rs rename to crates/librqbit/src/torrent_state/live/peer/mod.rs index a34633b..773be14 100644 --- a/crates/librqbit/src/peer_state.rs +++ b/crates/librqbit/src/torrent_state/live/peer/mod.rs @@ -1,3 +1,5 @@ +pub mod stats; + use std::collections::HashSet; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; @@ -13,6 +15,8 @@ use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use crate::peer_connection::WriterRequest; use crate::type_aliases::BF; +use super::peers::stats::atomic::AggregatePeerStatsAtomic; + #[derive(Debug, Hash, PartialEq, Eq)] pub struct InflightRequest { pub piece: ValidPieceIndex, @@ -45,85 +49,10 @@ impl SendMany for PeerTx { } } -#[derive(Default, Debug)] -pub struct PeerCounters { - pub fetched_bytes: AtomicU64, - pub total_time_connecting_ms: AtomicU64, - pub connection_attempts: AtomicU32, - pub connections: AtomicU32, - pub errors: AtomicU32, - pub fetched_chunks: AtomicU32, - pub downloaded_and_checked_pieces: AtomicU32, - pub downloaded_and_checked_bytes: AtomicU64, -} - -#[derive(Debug)] -pub struct PeerStats { - pub counters: Arc, - pub backoff: ExponentialBackoff, -} - -impl Default for PeerStats { - fn default() -> Self { - Self { - counters: Arc::new(Default::default()), - backoff: ExponentialBackoffBuilder::new() - .with_initial_interval(Duration::from_secs(10)) - .with_multiplier(6.) - .with_max_interval(Duration::from_secs(3600)) - .with_max_elapsed_time(Some(Duration::from_secs(86400))) - .build(), - } - } -} - #[derive(Debug, Default)] pub struct Peer { pub state: PeerStateNoMut, - pub stats: PeerStats, -} - -#[derive(Debug, Default, Serialize)] -pub struct AggregatePeerStatsAtomic { - pub queued: AtomicU32, - pub connecting: AtomicU32, - pub live: AtomicU32, - pub seen: AtomicU32, - pub dead: AtomicU32, - pub not_needed: AtomicU32, -} - -pub fn atomic_inc(c: &AtomicU32) -> u32 { - c.fetch_add(1, Ordering::Relaxed) -} - -pub fn atomic_dec(c: &AtomicU32) -> u32 { - c.fetch_sub(1, Ordering::Relaxed) -} - -impl AggregatePeerStatsAtomic { - pub fn counter(&self, state: &PeerState) -> &AtomicU32 { - match state { - PeerState::Connecting(_) => &self.connecting, - PeerState::Live(_) => &self.live, - PeerState::Queued => &self.queued, - PeerState::Dead => &self.dead, - PeerState::NotNeeded => &self.not_needed, - } - } - - pub fn inc(&self, state: &PeerState) { - atomic_inc(self.counter(state)); - } - - pub fn dec(&self, state: &PeerState) { - atomic_dec(self.counter(state)); - } - - pub fn incdec(&self, old: &PeerState, new: &PeerState) { - self.dec(old); - self.inc(new); - } + pub stats: stats::atomic::PeerStats, } #[derive(Debug, Default)] @@ -268,82 +197,3 @@ impl LivePeerState { .map_or(false, |s| s.all()) } } - -mod peer_stats_snapshot { - use std::{collections::HashMap, sync::atomic::Ordering}; - - use serde::{Deserialize, Serialize}; - - use crate::peer_state::PeerState; - - #[derive(Serialize, Deserialize)] - pub struct PeerCounters { - pub fetched_bytes: u64, - pub total_time_connecting_ms: u64, - pub connection_attempts: u32, - pub connections: u32, - pub errors: u32, - pub fetched_chunks: u32, - pub downloaded_and_checked_pieces: u32, - } - - #[derive(Serialize, Deserialize)] - pub struct PeerStats { - pub counters: PeerCounters, - pub state: &'static str, - } - - impl From<&crate::peer_state::PeerCounters> for PeerCounters { - fn from(counters: &crate::peer_state::PeerCounters) -> Self { - Self { - fetched_bytes: counters.fetched_bytes.load(Ordering::Relaxed), - total_time_connecting_ms: counters.total_time_connecting_ms.load(Ordering::Relaxed), - connection_attempts: counters.connection_attempts.load(Ordering::Relaxed), - connections: counters.connections.load(Ordering::Relaxed), - errors: counters.errors.load(Ordering::Relaxed), - fetched_chunks: counters.fetched_chunks.load(Ordering::Relaxed), - downloaded_and_checked_pieces: counters - .downloaded_and_checked_pieces - .load(Ordering::Relaxed), - } - } - } - - impl From<&crate::peer_state::Peer> for PeerStats { - fn from(peer: &crate::peer_state::Peer) -> Self { - Self { - counters: peer.stats.counters.as_ref().into(), - state: peer.state.get().name(), - } - } - } - - #[derive(Serialize)] - pub struct PeerStatsSnapshot { - pub peers: HashMap, - } - - #[derive(Clone, Copy, Default, Deserialize)] - pub enum PeerStatsFilterState { - All, - #[default] - Live, - } - - impl PeerStatsFilterState { - pub fn matches(&self, s: &PeerState) -> bool { - match (self, s) { - (Self::All, _) => true, - (Self::Live, PeerState::Live(_)) => true, - _ => false, - } - } - } - - #[derive(Default, Deserialize)] - pub struct PeerStatsFilter { - pub state: PeerStatsFilterState, - } -} - -pub use peer_stats_snapshot::{PeerStatsFilter, PeerStatsSnapshot}; diff --git a/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs b/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs new file mode 100644 index 0000000..bca260f --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs @@ -0,0 +1,41 @@ +use std::{ + sync::{ + atomic::{AtomicU32, AtomicU64}, + Arc, + }, + time::Duration, +}; + +use backoff::{ExponentialBackoff, ExponentialBackoffBuilder}; + +#[derive(Default, Debug)] +pub struct PeerCounters { + pub fetched_bytes: AtomicU64, + pub total_time_connecting_ms: AtomicU64, + pub connection_attempts: AtomicU32, + pub connections: AtomicU32, + pub errors: AtomicU32, + pub fetched_chunks: AtomicU32, + pub downloaded_and_checked_pieces: AtomicU32, + pub downloaded_and_checked_bytes: AtomicU64, +} + +#[derive(Debug)] +pub struct PeerStats { + pub counters: Arc, + pub backoff: ExponentialBackoff, +} + +impl Default for PeerStats { + fn default() -> Self { + Self { + counters: Arc::new(Default::default()), + backoff: ExponentialBackoffBuilder::new() + .with_initial_interval(Duration::from_secs(10)) + .with_multiplier(6.) + .with_max_interval(Duration::from_secs(3600)) + .with_max_elapsed_time(Some(Duration::from_secs(86400))) + .build(), + } + } +} diff --git a/crates/librqbit/src/torrent_state/live/peer/stats/mod.rs b/crates/librqbit/src/torrent_state/live/peer/stats/mod.rs new file mode 100644 index 0000000..1f2b657 --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/peer/stats/mod.rs @@ -0,0 +1,2 @@ +pub mod atomic; +pub mod snapshot; diff --git a/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs b/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs new file mode 100644 index 0000000..81c00cb --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs @@ -0,0 +1,74 @@ +use std::{collections::HashMap, sync::atomic::Ordering}; + +use serde::{Deserialize, Serialize}; + +use crate::torrent_state::live::peer::{Peer, PeerState}; + +#[derive(Serialize, Deserialize)] +pub struct PeerCounters { + pub fetched_bytes: u64, + pub total_time_connecting_ms: u64, + pub connection_attempts: u32, + pub connections: u32, + pub errors: u32, + pub fetched_chunks: u32, + pub downloaded_and_checked_pieces: u32, +} + +#[derive(Serialize, Deserialize)] +pub struct PeerStats { + pub counters: PeerCounters, + pub state: &'static str, +} + +impl From<&super::atomic::PeerCounters> for PeerCounters { + fn from(counters: &super::atomic::PeerCounters) -> Self { + Self { + fetched_bytes: counters.fetched_bytes.load(Ordering::Relaxed), + total_time_connecting_ms: counters.total_time_connecting_ms.load(Ordering::Relaxed), + connection_attempts: counters.connection_attempts.load(Ordering::Relaxed), + connections: counters.connections.load(Ordering::Relaxed), + errors: counters.errors.load(Ordering::Relaxed), + fetched_chunks: counters.fetched_chunks.load(Ordering::Relaxed), + downloaded_and_checked_pieces: counters + .downloaded_and_checked_pieces + .load(Ordering::Relaxed), + } + } +} + +impl From<&Peer> for PeerStats { + fn from(peer: &Peer) -> Self { + Self { + counters: peer.stats.counters.as_ref().into(), + state: peer.state.get().name(), + } + } +} + +#[derive(Serialize)] +pub struct PeerStatsSnapshot { + pub peers: HashMap, +} + +#[derive(Clone, Copy, Default, Deserialize)] +pub enum PeerStatsFilterState { + All, + #[default] + Live, +} + +impl PeerStatsFilterState { + pub fn matches(&self, s: &PeerState) -> bool { + match (self, s) { + (Self::All, _) => true, + (Self::Live, PeerState::Live(_)) => true, + _ => false, + } + } +} + +#[derive(Default, Deserialize)] +pub struct PeerStatsFilter { + pub state: PeerStatsFilterState, +} diff --git a/crates/librqbit/src/torrent_state/live/peers/mod.rs b/crates/librqbit/src/torrent_state/live/peers/mod.rs new file mode 100644 index 0000000..7973a27 --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/peers/mod.rs @@ -0,0 +1,114 @@ +use std::net::SocketAddr; + +use anyhow::Context; +use backoff::backoff::Backoff; +use dashmap::DashMap; + +use crate::{ + torrent_state::utils::{atomic_inc, TimedExistence}, + type_aliases::{PeerHandle, BF}, +}; + +use self::stats::{atomic::AggregatePeerStatsAtomic, snapshot::AggregatePeerStats}; + +use super::peer::{LivePeerState, Peer, PeerRx, PeerState, PeerTx}; + +pub mod stats; + +#[derive(Default)] +pub struct PeerStates { + pub stats: AggregatePeerStatsAtomic, + pub states: DashMap, +} + +impl PeerStates { + pub fn stats(&self) -> AggregatePeerStats { + AggregatePeerStats::from(&self.stats) + } + + pub fn add_if_not_seen(&self, addr: SocketAddr) -> Option { + use dashmap::mapref::entry::Entry; + match self.states.entry(addr) { + Entry::Occupied(_) => None, + Entry::Vacant(vac) => { + vac.insert(Default::default()); + atomic_inc(&self.stats.queued); + atomic_inc(&self.stats.seen); + Some(addr) + } + } + } + pub fn with_peer(&self, addr: PeerHandle, f: impl FnOnce(&Peer) -> R) -> Option { + self.states.get(&addr).map(|e| f(e.value())) + } + + pub fn with_peer_mut( + &self, + addr: PeerHandle, + reason: &'static str, + f: impl FnOnce(&mut Peer) -> R, + ) -> Option { + use crate::torrent_state::utils::timeit; + timeit(reason, || self.states.get_mut(&addr)) + .map(|e| f(TimedExistence::new(e, reason).value_mut())) + } + pub fn with_live(&self, addr: PeerHandle, f: impl FnOnce(&LivePeerState) -> R) -> Option { + self.states + .get(&addr) + .and_then(|e| match &e.value().state.get() { + PeerState::Live(l) => Some(f(l)), + _ => None, + }) + } + pub fn with_live_mut( + &self, + addr: PeerHandle, + reason: &'static str, + f: impl FnOnce(&mut LivePeerState) -> R, + ) -> Option { + self.with_peer_mut(addr, reason, |peer| peer.state.get_live_mut().map(f)) + .flatten() + } + + pub fn drop_peer(&self, handle: PeerHandle) -> Option { + let p = self.states.remove(&handle).map(|r| r.1)?; + self.stats.dec(p.state.get()); + Some(p) + } + + pub fn mark_peer_interested(&self, handle: PeerHandle, is_interested: bool) -> Option { + self.with_live_mut(handle, "mark_peer_interested", |live| { + let prev = live.peer_interested; + live.peer_interested = is_interested; + prev + }) + } + pub fn update_bitfield_from_vec(&self, handle: PeerHandle, bitfield: Vec) -> Option<()> { + self.with_live_mut(handle, "update_bitfield_from_vec", |live| { + live.bitfield = BF::from_vec(bitfield); + }) + } + pub fn mark_peer_connecting(&self, h: PeerHandle) -> anyhow::Result<(PeerRx, PeerTx)> { + let rx = self + .with_peer_mut(h, "mark_peer_connecting", |peer| { + peer.state + .queued_to_connecting(&self.stats) + .context("invalid peer state") + }) + .context("peer not found in states")??; + Ok(rx) + } + + pub fn reset_peer_backoff(&self, handle: PeerHandle) { + self.with_peer_mut(handle, "reset_peer_backoff", |p| { + p.stats.backoff.reset(); + }); + } + + pub fn mark_peer_not_needed(&self, handle: PeerHandle) -> Option { + let prev = self.with_peer_mut(handle, "mark_peer_not_needed", |peer| { + peer.state.to_not_needed(&self.stats) + })?; + Some(prev) + } +} diff --git a/crates/librqbit/src/torrent_state/live/peers/stats/atomic.rs b/crates/librqbit/src/torrent_state/live/peers/stats/atomic.rs new file mode 100644 index 0000000..a9188c3 --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/peers/stats/atomic.rs @@ -0,0 +1,43 @@ +use std::sync::atomic::AtomicU32; + +use serde::Serialize; + +use crate::torrent_state::{ + live::peer::PeerState, + utils::{atomic_dec, atomic_inc}, +}; + +#[derive(Debug, Default, Serialize)] +pub struct AggregatePeerStatsAtomic { + pub queued: AtomicU32, + pub connecting: AtomicU32, + pub live: AtomicU32, + pub seen: AtomicU32, + pub dead: AtomicU32, + pub not_needed: AtomicU32, +} + +impl AggregatePeerStatsAtomic { + pub fn counter(&self, state: &PeerState) -> &AtomicU32 { + match state { + PeerState::Connecting(_) => &self.connecting, + PeerState::Live(_) => &self.live, + PeerState::Queued => &self.queued, + PeerState::Dead => &self.dead, + PeerState::NotNeeded => &self.not_needed, + } + } + + pub fn inc(&self, state: &PeerState) { + atomic_inc(self.counter(state)); + } + + pub fn dec(&self, state: &PeerState) { + atomic_dec(self.counter(state)); + } + + pub fn incdec(&self, old: &PeerState, new: &PeerState) { + self.dec(old); + self.inc(new); + } +} diff --git a/crates/librqbit/src/torrent_state/live/peers/stats/mod.rs b/crates/librqbit/src/torrent_state/live/peers/stats/mod.rs new file mode 100644 index 0000000..1f2b657 --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/peers/stats/mod.rs @@ -0,0 +1,2 @@ +pub mod atomic; +pub mod snapshot; diff --git a/crates/librqbit/src/torrent_state/live/peers/stats/snapshot.rs b/crates/librqbit/src/torrent_state/live/peers/stats/snapshot.rs new file mode 100644 index 0000000..a42ad2c --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/peers/stats/snapshot.rs @@ -0,0 +1,29 @@ +use std::sync::atomic::Ordering; + +use serde::Serialize; + +use super::atomic::AggregatePeerStatsAtomic; + +#[derive(Debug, Default, Serialize, PartialEq, Eq)] +pub struct AggregatePeerStats { + pub queued: usize, + pub connecting: usize, + pub live: usize, + pub seen: usize, + pub dead: usize, + pub not_needed: usize, +} + +impl<'a> From<&'a AggregatePeerStatsAtomic> for AggregatePeerStats { + fn from(s: &'a AggregatePeerStatsAtomic) -> Self { + let ordering = Ordering::Relaxed; + Self { + queued: s.queued.load(ordering) as usize, + connecting: s.connecting.load(ordering) as usize, + live: s.live.load(ordering) as usize, + seen: s.seen.load(ordering) as usize, + dead: s.dead.load(ordering) as usize, + not_needed: s.not_needed.load(ordering) as usize, + } + } +} diff --git a/crates/librqbit/src/torrent_state/live/stats/atomic.rs b/crates/librqbit/src/torrent_state/live/stats/atomic.rs new file mode 100644 index 0000000..4e3c024 --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/stats/atomic.rs @@ -0,0 +1,25 @@ +use std::{ + sync::atomic::{AtomicU64, Ordering}, + time::Duration, +}; + +#[derive(Default, Debug)] +pub struct AtomicStats { + pub have_bytes: AtomicU64, + pub downloaded_and_checked_bytes: AtomicU64, + pub downloaded_and_checked_pieces: AtomicU64, + pub uploaded_bytes: AtomicU64, + pub fetched_bytes: AtomicU64, + pub total_piece_download_ms: AtomicU64, +} + +impl AtomicStats { + pub fn average_piece_download_time(&self) -> Option { + let d = self.downloaded_and_checked_pieces.load(Ordering::Acquire); + let t = self.total_piece_download_ms.load(Ordering::Acquire); + if d == 0 { + return None; + } + Some(Duration::from_secs_f64(t as f64 / d as f64 / 1000f64)) + } +} diff --git a/crates/librqbit/src/torrent_state/live/stats/mod.rs b/crates/librqbit/src/torrent_state/live/stats/mod.rs new file mode 100644 index 0000000..1f2b657 --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/stats/mod.rs @@ -0,0 +1,2 @@ +pub mod atomic; +pub mod snapshot; diff --git a/crates/librqbit/src/torrent_state/live/stats/snapshot.rs b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs new file mode 100644 index 0000000..f22b8be --- /dev/null +++ b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs @@ -0,0 +1,32 @@ +use std::time::{Duration, Instant}; + +use serde::Serialize; + +use crate::torrent_state::live::peers::stats::snapshot::AggregatePeerStats; + +#[derive(Debug, Serialize)] +pub struct StatsSnapshot { + pub have_bytes: u64, + pub downloaded_and_checked_bytes: u64, + pub downloaded_and_checked_pieces: u64, + pub fetched_bytes: u64, + pub uploaded_bytes: u64, + pub initially_needed_bytes: u64, + pub remaining_bytes: u64, + pub total_bytes: u64, + #[serde(skip)] + pub time: Instant, + pub total_piece_download_ms: u64, + pub peer_stats: AggregatePeerStats, +} + +impl StatsSnapshot { + pub fn average_piece_download_time(&self) -> Option { + let d = self.downloaded_and_checked_pieces; + let t = self.total_piece_download_ms; + if d == 0 { + return None; + } + Some(Duration::from_secs_f64(t as f64 / d as f64 / 1000f64)) + } +} diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 0ac1567..c1c0d09 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -1,3 +1,5 @@ +pub mod utils; + pub mod live; pub use live::*; diff --git a/crates/librqbit/src/torrent_state/utils.rs b/crates/librqbit/src/torrent_state/utils.rs new file mode 100644 index 0000000..3323cba --- /dev/null +++ b/crates/librqbit/src/torrent_state/utils.rs @@ -0,0 +1,108 @@ +use std::sync::atomic::{AtomicU32, Ordering}; + +pub fn atomic_inc(c: &AtomicU32) -> u32 { + c.fetch_add(1, Ordering::Relaxed) +} + +pub fn atomic_dec(c: &AtomicU32) -> u32 { + c.fetch_sub(1, Ordering::Relaxed) +} + +// Used during debugging to see if some locks take too long. +#[cfg(not(feature = "timed_existence"))] +mod timed_existence { + use std::ops::{Deref, DerefMut}; + + pub struct TimedExistence(T); + + impl TimedExistence { + #[inline(always)] + pub fn new(object: T, _reason: &'static str) -> Self { + Self(object) + } + } + + impl Deref for TimedExistence { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for TimedExistence { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + #[inline(always)] + pub fn timeit(_n: impl std::fmt::Display, f: impl FnOnce() -> R) -> R { + f() + } +} + +#[cfg(feature = "timed_existence")] +mod timed_existence { + use std::ops::{Deref, DerefMut}; + use std::time::{Duration, Instant}; + use tracing::warn; + + const MAX: Duration = Duration::from_millis(1); + + // Prints if the object exists for too long. + // This is used to track long-lived locks for debugging. + pub struct TimedExistence { + object: T, + reason: &'static str, + started: Instant, + } + + impl TimedExistence { + pub fn new(object: T, reason: &'static str) -> Self { + Self { + object, + reason, + started: Instant::now(), + } + } + } + + impl Drop for TimedExistence { + fn drop(&mut self) { + let elapsed = self.started.elapsed(); + let reason = self.reason; + if elapsed > MAX { + warn!("elapsed on lock {reason:?}: {elapsed:?}") + } + } + } + + impl Deref for TimedExistence { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.object + } + } + + impl DerefMut for TimedExistence { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.object + } + } + + pub fn timeit(name: impl std::fmt::Display, f: impl FnOnce() -> R) -> R { + let now = Instant::now(); + let r = f(); + let elapsed = now.elapsed(); + if elapsed > MAX { + warn!("elapsed on \"{name:}\": {elapsed:?}") + } + r + } +} + +pub use timed_existence::{timeit, TimedExistence}; diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 33fd911..e457e36 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -11,7 +11,6 @@ use librqbit::{ Session, SessionOptions, }, spawn_utils::{spawn, BlockingSpawner}, - torrent_state::timeit, }; use size_format::SizeFormatterBinary as SF; use tracing::{error, info, span, warn, Level}; @@ -244,7 +243,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> info!("[{}] initializing", idx); }, ManagedTorrentState::Running(handle) => { - let stats = timeit("stats_snapshot", || handle.torrent_state().stats_snapshot()); + let stats = handle.torrent_state().stats_snapshot(); let speed = handle.speed_estimator(); let total = stats.total_bytes; let progress = stats.total_bytes - stats.remaining_bytes; From 84766f92fbc26c8272ef44dd5ad199fb2029adf1 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Thu, 23 Nov 2023 16:28:26 +0000 Subject: [PATCH 04/38] cargo fix --- crates/librqbit/src/file_ops.rs | 2 +- crates/librqbit/src/torrent_state/live/peer/mod.rs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/librqbit/src/file_ops.rs b/crates/librqbit/src/file_ops.rs index 32e2bb3..8136c9f 100644 --- a/crates/librqbit/src/file_ops.rs +++ b/crates/librqbit/src/file_ops.rs @@ -11,10 +11,10 @@ use librqbit_core::{ lengths::{ChunkInfo, Lengths, ValidPieceIndex}, torrent_metainfo::{FileIteratorName, TorrentMetaV1Info}, }; -use tracing::{debug, trace, warn}; use parking_lot::Mutex; use peer_binary_protocol::Piece; use sha1w::ISha1; +use tracing::{debug, trace, warn}; use crate::type_aliases::{PeerHandle, BF}; diff --git a/crates/librqbit/src/torrent_state/live/peer/mod.rs b/crates/librqbit/src/torrent_state/live/peer/mod.rs index 773be14..9b16d5e 100644 --- a/crates/librqbit/src/torrent_state/live/peer/mod.rs +++ b/crates/librqbit/src/torrent_state/live/peer/mod.rs @@ -1,15 +1,12 @@ pub mod stats; use std::collections::HashSet; -use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; -use std::sync::Arc; -use std::time::Duration; use anyhow::Context; -use backoff::{ExponentialBackoff, ExponentialBackoffBuilder}; + use librqbit_core::id20::Id20; use librqbit_core::lengths::{ChunkInfo, ValidPieceIndex}; -use serde::Serialize; + use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use crate::peer_connection::WriterRequest; From f45a15c89aaf635a44be25f07c466d2a8582ca2a Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Thu, 23 Nov 2023 16:57:27 +0000 Subject: [PATCH 05/38] Downgrade visibility within librqbit --- crates/librqbit/src/chunk_tracker.rs | 15 ++------- crates/librqbit/src/file_ops.rs | 4 +-- crates/librqbit/src/http_api.rs | 2 +- crates/librqbit/src/torrent_state/live/mod.rs | 31 ++++++++++--------- .../src/torrent_state/live/peer/mod.rs | 31 +++++++------------ .../torrent_state/live/peer/stats/atomic.rs | 6 ++-- .../torrent_state/live/peer/stats/snapshot.rs | 12 +++---- .../src/torrent_state/live/peers/mod.rs | 13 ++------ .../torrent_state/live/peers/stats/atomic.rs | 2 +- 9 files changed, 44 insertions(+), 72 deletions(-) diff --git a/crates/librqbit/src/chunk_tracker.rs b/crates/librqbit/src/chunk_tracker.rs index e223d06..880ce53 100644 --- a/crates/librqbit/src/chunk_tracker.rs +++ b/crates/librqbit/src/chunk_tracker.rs @@ -4,7 +4,7 @@ use tracing::{debug, info}; use crate::type_aliases::BF; -pub struct ChunkTracker { +pub(crate) struct ChunkTracker { // This forms the basis of a "queue" to pull from. // It's set to 1 if we need a piece, but the moment we start requesting a peer, // it's set to 0. @@ -51,7 +51,7 @@ fn compute_chunk_status(lengths: &Lengths, needed_pieces: &BF) -> BF { chunk_bf } -pub enum ChunkMarkingResult { +pub(crate) enum ChunkMarkingResult { PreviouslyCompleted, NotCompleted, Completed, @@ -75,9 +75,7 @@ impl ChunkTracker { priority_piece_ids, } } - pub fn get_needed_pieces(&self) -> &BF { - &self.needed_pieces - } + pub fn get_have_pieces(&self) -> &BF { &self.have } @@ -132,13 +130,6 @@ impl ChunkTracker { self.have.set(idx.get() as usize, true); } - pub fn is_chunk_downloaded(&self, chunk: &ChunkInfo) -> bool { - *self - .chunk_status - .get(chunk.absolute_index as usize) - .unwrap() - } - pub fn is_chunk_ready_to_upload(&self, chunk: &ChunkInfo) -> bool { self.have .get(chunk.piece_index.get() as usize) diff --git a/crates/librqbit/src/file_ops.rs b/crates/librqbit/src/file_ops.rs index 8136c9f..e413dc4 100644 --- a/crates/librqbit/src/file_ops.rs +++ b/crates/librqbit/src/file_ops.rs @@ -18,7 +18,7 @@ use tracing::{debug, trace, warn}; use crate::type_aliases::{PeerHandle, BF}; -pub struct InitialCheckResults { +pub(crate) struct InitialCheckResults { pub needed_pieces: BF, pub have_pieces: BF, pub have_bytes: u64, @@ -43,7 +43,7 @@ pub fn update_hash_from_file( Ok(()) } -pub struct FileOps<'a, Sha1> { +pub(crate) struct FileOps<'a, Sha1> { torrent: &'a TorrentMetaV1Info, files: &'a [Arc>], lengths: &'a Lengths, diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 11395d8..2249c4f 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -330,7 +330,7 @@ impl TorrentAddQueryParams { } // Private HTTP API internals. Agnostic of web framework. -pub struct ApiInternal { +struct ApiInternal { dht: Option, startup_time: Instant, torrent_managers: RwLock>, diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index fa49040..b2be121 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -92,7 +92,7 @@ use crate::{ use self::{ peer::{ stats::{ - atomic::PeerCounters as AtomicPeerCounters, + atomic::PeerCountersAtomic as AtomicPeerCounters, snapshot::{PeerStatsFilter, PeerStatsSnapshot}, }, InflightRequest, PeerState, PeerTx, SendMany, @@ -103,18 +103,18 @@ use self::{ use super::utils::{timeit, TimedExistence}; -pub struct InflightPiece { - pub peer: PeerHandle, - pub started: Instant, +struct InflightPiece { + peer: PeerHandle, + started: Instant, } -pub struct TorrentStateLocked { +pub(crate) struct TorrentStateLocked { // What chunks we have and need. - pub chunks: ChunkTracker, + pub(crate) chunks: ChunkTracker, // At a moment in time, we are expecting a piece from only one peer. // inflight_pieces stores this information. - pub inflight_pieces: HashMap, + inflight_pieces: HashMap, } #[derive(Default)] @@ -148,7 +148,7 @@ pub struct TorrentState { impl TorrentState { #[allow(clippy::too_many_arguments)] - pub fn new( + pub(crate) fn new( info: TorrentMetaV1Info, info_hash: Id20, peer_id: Id20, @@ -194,7 +194,7 @@ impl TorrentState { state } - pub async fn task_manage_peer( + async fn task_manage_peer( self: Arc, addr: SocketAddr, spawner: BlockingSpawner, @@ -260,7 +260,7 @@ impl TorrentState { Ok::<_, anyhow::Error>(()) } - pub async fn task_peer_adder( + async fn task_peer_adder( self: Arc, mut peer_queue_rx: UnboundedReceiver, spawner: BlockingSpawner, @@ -292,19 +292,20 @@ impl TorrentState { pub fn peer_id(&self) -> Id20 { self.peer_id } - pub fn file_ops(&self) -> FileOps<'_, Sha1> { + pub(crate) fn file_ops(&self) -> FileOps<'_, Sha1> { FileOps::new(&self.info, &self.files, &self.lengths) } pub fn initially_needed(&self) -> u64 { self.needed_bytes } - pub fn lock_read( + + pub(crate) fn lock_read( &self, reason: &'static str, ) -> TimedExistence> { TimedExistence::new(timeit(reason, || self.locked.read()), reason) } - pub fn lock_write( + pub(crate) fn lock_write( &self, reason: &'static str, ) -> TimedExistence> { @@ -417,7 +418,7 @@ impl TorrentState { ); } - pub fn add_peer_if_not_seen(self: &Arc, addr: SocketAddr) -> bool { + pub(crate) fn add_peer_if_not_seen(self: &Arc, addr: SocketAddr) -> bool { match self.peers.add_if_not_seen(addr) { Some(handle) => handle, None => return false, @@ -1171,7 +1172,7 @@ impl PeerHandler { for mut pe in self.state.peers.states.iter_mut() { if let PeerState::Live(l) = pe.value().state.get() { if l.has_full_torrent(self.state.lengths.total_pieces() as usize) { - let prev = pe.value_mut().state.to_not_needed(&self.state.peers.stats); + let prev = pe.value_mut().state.set_not_needed(&self.state.peers.stats); let _ = prev .take_live_no_counters() .unwrap() diff --git a/crates/librqbit/src/torrent_state/live/peer/mod.rs b/crates/librqbit/src/torrent_state/live/peer/mod.rs index 9b16d5e..675d762 100644 --- a/crates/librqbit/src/torrent_state/live/peer/mod.rs +++ b/crates/librqbit/src/torrent_state/live/peer/mod.rs @@ -15,7 +15,7 @@ use crate::type_aliases::BF; use super::peers::stats::atomic::AggregatePeerStatsAtomic; #[derive(Debug, Hash, PartialEq, Eq)] -pub struct InflightRequest { +pub(crate) struct InflightRequest { pub piece: ValidPieceIndex, pub chunk: u32, } @@ -30,8 +30,8 @@ impl From<&ChunkInfo> for InflightRequest { } // TODO: Arc can be removed probably, as UnboundedSender should be clone + it can be downgraded to weak. -pub type PeerRx = UnboundedReceiver; -pub type PeerTx = UnboundedSender; +pub(crate) type PeerRx = UnboundedReceiver; +pub(crate) type PeerTx = UnboundedSender; pub trait SendMany { fn send_many(&self, requests: impl IntoIterator) -> anyhow::Result<()>; @@ -47,13 +47,13 @@ impl SendMany for PeerTx { } #[derive(Debug, Default)] -pub struct Peer { +pub(crate) struct Peer { pub state: PeerStateNoMut, pub stats: stats::atomic::PeerStats, } #[derive(Debug, Default)] -pub enum PeerState { +pub(crate) enum PeerState { #[default] // Will be tried to be connected as soon as possible. Queued, @@ -93,7 +93,7 @@ impl PeerState { } #[derive(Debug, Default)] -pub struct PeerStateNoMut(PeerState); +pub(crate) struct PeerStateNoMut(PeerState); impl PeerStateNoMut { pub fn get(&self) -> &PeerState { @@ -109,13 +109,6 @@ impl PeerStateNoMut { std::mem::replace(&mut self.0, new) } - pub fn get_live(&self) -> Option<&LivePeerState> { - match &self.0 { - PeerState::Live(l) => Some(l), - _ => None, - } - } - pub fn get_live_mut(&mut self) -> Option<&mut LivePeerState> { match &mut self.0 { PeerState::Live(l) => Some(l), @@ -153,18 +146,16 @@ impl PeerStateNoMut { } } - pub fn to_dead(&mut self, counters: &AggregatePeerStatsAtomic) -> PeerState { - self.set(PeerState::Dead, counters) - } - - pub fn to_not_needed(&mut self, counters: &AggregatePeerStatsAtomic) -> PeerState { + pub fn set_not_needed(&mut self, counters: &AggregatePeerStatsAtomic) -> PeerState { self.set(PeerState::NotNeeded, counters) } } #[derive(Debug)] -pub struct LivePeerState { - pub peer_id: Id20, +pub(crate) struct LivePeerState { + #[allow(dead_code)] + peer_id: Id20, + pub peer_interested: bool, // This is used to track the pieces the peer has. diff --git a/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs b/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs index bca260f..6c9b80a 100644 --- a/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs +++ b/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs @@ -9,7 +9,7 @@ use std::{ use backoff::{ExponentialBackoff, ExponentialBackoffBuilder}; #[derive(Default, Debug)] -pub struct PeerCounters { +pub(crate) struct PeerCountersAtomic { pub fetched_bytes: AtomicU64, pub total_time_connecting_ms: AtomicU64, pub connection_attempts: AtomicU32, @@ -21,8 +21,8 @@ pub struct PeerCounters { } #[derive(Debug)] -pub struct PeerStats { - pub counters: Arc, +pub(crate) struct PeerStats { + pub counters: Arc, pub backoff: ExponentialBackoff, } diff --git a/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs b/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs index 81c00cb..48db933 100644 --- a/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs +++ b/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs @@ -21,8 +21,8 @@ pub struct PeerStats { pub state: &'static str, } -impl From<&super::atomic::PeerCounters> for PeerCounters { - fn from(counters: &super::atomic::PeerCounters) -> Self { +impl From<&super::atomic::PeerCountersAtomic> for PeerCounters { + fn from(counters: &super::atomic::PeerCountersAtomic) -> Self { Self { fetched_bytes: counters.fetched_bytes.load(Ordering::Relaxed), total_time_connecting_ms: counters.total_time_connecting_ms.load(Ordering::Relaxed), @@ -59,12 +59,8 @@ pub enum PeerStatsFilterState { } impl PeerStatsFilterState { - pub fn matches(&self, s: &PeerState) -> bool { - match (self, s) { - (Self::All, _) => true, - (Self::Live, PeerState::Live(_)) => true, - _ => false, - } + pub(crate) fn matches(&self, s: &PeerState) -> bool { + matches!((self, s), (Self::All, _) | (Self::Live, PeerState::Live(_))) } } diff --git a/crates/librqbit/src/torrent_state/live/peers/mod.rs b/crates/librqbit/src/torrent_state/live/peers/mod.rs index 7973a27..df359b8 100644 --- a/crates/librqbit/src/torrent_state/live/peers/mod.rs +++ b/crates/librqbit/src/torrent_state/live/peers/mod.rs @@ -16,7 +16,7 @@ use super::peer::{LivePeerState, Peer, PeerRx, PeerState, PeerTx}; pub mod stats; #[derive(Default)] -pub struct PeerStates { +pub(crate) struct PeerStates { pub stats: AggregatePeerStatsAtomic, pub states: DashMap, } @@ -52,14 +52,7 @@ impl PeerStates { timeit(reason, || self.states.get_mut(&addr)) .map(|e| f(TimedExistence::new(e, reason).value_mut())) } - pub fn with_live(&self, addr: PeerHandle, f: impl FnOnce(&LivePeerState) -> R) -> Option { - self.states - .get(&addr) - .and_then(|e| match &e.value().state.get() { - PeerState::Live(l) => Some(f(l)), - _ => None, - }) - } + pub fn with_live_mut( &self, addr: PeerHandle, @@ -107,7 +100,7 @@ impl PeerStates { pub fn mark_peer_not_needed(&self, handle: PeerHandle) -> Option { let prev = self.with_peer_mut(handle, "mark_peer_not_needed", |peer| { - peer.state.to_not_needed(&self.stats) + peer.state.set_not_needed(&self.stats) })?; Some(prev) } diff --git a/crates/librqbit/src/torrent_state/live/peers/stats/atomic.rs b/crates/librqbit/src/torrent_state/live/peers/stats/atomic.rs index a9188c3..9c9605e 100644 --- a/crates/librqbit/src/torrent_state/live/peers/stats/atomic.rs +++ b/crates/librqbit/src/torrent_state/live/peers/stats/atomic.rs @@ -8,7 +8,7 @@ use crate::torrent_state::{ }; #[derive(Debug, Default, Serialize)] -pub struct AggregatePeerStatsAtomic { +pub(crate) struct AggregatePeerStatsAtomic { pub queued: AtomicU32, pub connecting: AtomicU32, pub live: AtomicU32, From cc1ef9d0e4fcd8dbf730f2bf45951b9d57fa3a40 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Thu, 23 Nov 2023 17:14:08 +0000 Subject: [PATCH 06/38] Create ManagedTorrent struct --- crates/librqbit/src/torrent_manager.rs | 8 ++--- crates/librqbit/src/torrent_state/live/mod.rs | 8 ++--- crates/librqbit/src/torrent_state/mod.rs | 32 +++++++++++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/crates/librqbit/src/torrent_manager.rs b/crates/librqbit/src/torrent_manager.rs index b953530..4be9c9c 100644 --- a/crates/librqbit/src/torrent_manager.rs +++ b/crates/librqbit/src/torrent_manager.rs @@ -24,7 +24,7 @@ use crate::{ chunk_tracker::ChunkTracker, file_ops::FileOps, spawn_utils::{spawn, BlockingSpawner}, - torrent_state::{TorrentState, TorrentStateOptions}, + torrent_state::{TorrentStateLive, TorrentStateOptions}, tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, }; @@ -131,7 +131,7 @@ impl TorrentManagerHandle { pub fn add_peer(&self, addr: SocketAddr) -> bool { self.manager.state.add_peer_if_not_seen(addr) } - pub fn torrent_state(&self) -> &TorrentState { + pub fn torrent_state(&self) -> &TorrentStateLive { &self.manager.state } pub fn speed_estimator(&self) -> &Arc { @@ -147,7 +147,7 @@ impl TorrentManagerHandle { } struct TorrentManager { - state: Arc, + state: Arc, #[allow(dead_code)] speed_estimator: Arc, trackers: Mutex>, @@ -267,7 +267,7 @@ impl TorrentManager { ..Default::default() }; - let state = TorrentState::new( + let state = TorrentStateLive::new( info, info_hash, peer_id, diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index b2be121..6335f6f 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -123,7 +123,7 @@ pub struct TorrentStateOptions { pub peer_read_write_timeout: Option, } -pub struct TorrentState { +pub struct TorrentStateLive { peers: PeerStates, info: TorrentMetaV1Info, locked: Arc>, @@ -146,7 +146,7 @@ pub struct TorrentState { finished_notify: Notify, } -impl TorrentState { +impl TorrentStateLive { #[allow(clippy::too_many_arguments)] pub(crate) fn new( info: TorrentMetaV1Info, @@ -163,7 +163,7 @@ impl TorrentState { ) -> Arc { let options = options.unwrap_or_default(); let (peer_queue_tx, peer_queue_rx) = unbounded_channel(); - let state = Arc::new(TorrentState { + let state = Arc::new(TorrentStateLive { info_hash, info, peer_id, @@ -478,7 +478,7 @@ struct PeerHandlerLocked { // All peer state that would never be used by other actors should pe put here. // This state tracks a live peer. struct PeerHandler { - state: Arc, + state: Arc, counters: Arc, // Semantically, we don't need an RwLock here, as this is only requested from // one future (requester + manage_peer). diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index c1c0d09..6b8347b 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -2,4 +2,36 @@ pub mod utils; pub mod live; +use std::sync::Arc; + +use buffers::ByteString; +use librqbit_core::id20::Id20; +use librqbit_core::torrent_metainfo::TorrentMetaV1Info; pub use live::*; +use parking_lot::RwLock; +use tokio::sync::mpsc::Sender; +use url::Url; + +pub(crate) enum ManagedTorrentState { + Live { + state: TorrentStateLive, + only_files_tx: Sender>, + trackers_tx: Sender, + }, +} + +pub(crate) struct ManagedTorrentLocked { + pub trackers: Vec, + pub only_files: Vec, + pub state: ManagedTorrentState, +} + +pub struct ManagedTorrentInfo { + pub info: TorrentMetaV1Info, + pub info_hash: Id20, +} + +pub(crate) struct ManagedTorrent { + pub info: Arc, + pub(crate) locked: RwLock, +} From 739666ff8899076cdffa0caf5068e9614f102fbd Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 09:30:21 +0000 Subject: [PATCH 07/38] HUGE REFACTOR to suppor multiple states. Incomplete, broken --- crates/librqbit/src/http_api.rs | 55 +++--- crates/librqbit/src/lib.rs | 2 +- crates/librqbit/src/session.rs | 105 +++-------- crates/librqbit/src/torrent_manager.rs | 124 +------------ crates/librqbit/src/torrent_state/live/mod.rs | 11 ++ crates/librqbit/src/torrent_state/mod.rs | 172 +++++++++++++++++- crates/rqbit/src/main.rs | 17 +- 7 files changed, 242 insertions(+), 244 deletions(-) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 2249c4f..e86e31d 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -1,4 +1,4 @@ -use anyhow::Context; +use anyhow::{bail, Context}; use axum::body::Bytes; use axum::extract::{Path, Query, State}; use axum::response::IntoResponse; @@ -22,9 +22,9 @@ use crate::http_api_error::{ApiError, ApiErrorExt}; use crate::session::{ AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, }; -use crate::torrent_manager::TorrentManagerHandle; use crate::torrent_state::peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot}; use crate::torrent_state::stats::snapshot::StatsSnapshot; +use crate::torrent_state::ManagedTorrentHandle; // Public API #[derive(Clone)] @@ -38,7 +38,7 @@ impl HttpApi { inner: Arc::new(ApiInternal::new(session)), } } - pub fn add_torrent_handle(&self, handle: TorrentManagerHandle) -> usize { + pub fn add_torrent_handle(&self, handle: ManagedTorrentHandle) -> usize { self.inner.add_torrent_handle(handle) } @@ -333,7 +333,7 @@ impl TorrentAddQueryParams { struct ApiInternal { dht: Option, startup_time: Instant, - torrent_managers: RwLock>, + torrent_managers: RwLock>, session: Arc, } @@ -349,14 +349,14 @@ impl ApiInternal { } } - fn add_torrent_handle(&self, handle: TorrentManagerHandle) -> usize { + fn add_torrent_handle(&self, handle: ManagedTorrentHandle) -> usize { let mut g = self.torrent_managers.write(); let idx = g.len(); g.push(handle); idx } - fn mgr_handle(&self, idx: usize) -> Result { + fn mgr_handle(&self, idx: usize) -> Result { self.torrent_managers .read() .get(idx) @@ -373,7 +373,7 @@ impl ApiInternal { .enumerate() .map(|(id, mgr)| TorrentListResponseItem { id, - info_hash: mgr.torrent_state().info_hash().as_string(), + info_hash: mgr.info().info_hash.as_string(), }) .collect(), } @@ -381,14 +381,17 @@ impl ApiInternal { fn api_torrent_details(&self, idx: usize) -> Result { let handle = self.mgr_handle(idx)?; - let info_hash = handle.torrent_state().info_hash(); + let info_hash = handle.info().info_hash; let only_files = handle.only_files(); - make_torrent_details(&info_hash, handle.torrent_state().info(), only_files) + make_torrent_details(&info_hash, &handle.info().info, only_files.as_deref()) } fn api_peer_stats(&self, idx: usize, filter: PeerStatsFilter) -> Result { let handle = self.mgr_handle(idx)?; - Ok(handle.torrent_state().per_peer_stats_snapshot(filter)) + Ok(handle + .live() + .context("not live")? + .per_peer_stats_snapshot(filter)) } pub async fn api_add_torrent( @@ -406,8 +409,8 @@ impl ApiInternal { AddTorrentResponse::AlreadyManaged(managed) => { return Err(anyhow::anyhow!( "{:?} is already managed, downloaded to {:?}", - managed.info_hash, - managed.output_folder + managed.info_hash(), + &managed.info().out_dir )) .with_error_status_code(StatusCode::CONFLICT); } @@ -422,9 +425,9 @@ impl ApiInternal { }, AddTorrentResponse::Added(handle) => { let details = make_torrent_details( - &handle.torrent_state().info_hash(), - handle.torrent_state().info(), - handle.only_files(), + &handle.info_hash(), + &handle.info().info, + handle.only_files().as_deref(), ) .context("error making torrent details")?; let id = self.add_torrent_handle(handle); @@ -451,8 +454,9 @@ impl ApiInternal { fn api_stats(&self, idx: usize) -> Result { let mgr = self.mgr_handle(idx)?; - let snapshot = mgr.torrent_state().stats_snapshot(); - let estimator = mgr.speed_estimator(); + let live = mgr.live().context("not live")?; + let snapshot = live.stats_snapshot(); + let estimator = live.speed_estimator(); // Poor mans download speed computation let elapsed = self.startup_time.elapsed(); @@ -469,14 +473,15 @@ impl ApiInternal { } fn api_dump_haves(&self, idx: usize) -> Result { - let mgr = self.mgr_handle(idx)?; - Ok(format!( - "{:?}", - mgr.torrent_state() - .lock_read("api_dump_haves") - .chunks - .get_have_pieces(), - )) + Err(anyhow::anyhow!("not implemented").into()) + // let mgr = self.mgr_handle(idx)?; + // Ok(format!( + // "{:?}", + // mgr.live().conetext() + // .lock_read("api_dump_haves") + // .chunks + // .get_have_pieces(), + // )) } } diff --git a/crates/librqbit/src/lib.rs b/crates/librqbit/src/lib.rs index 78d44aa..4be7c2d 100644 --- a/crates/librqbit/src/lib.rs +++ b/crates/librqbit/src/lib.rs @@ -8,7 +8,7 @@ pub mod peer_connection; pub mod peer_info_reader; pub mod session; pub mod spawn_utils; -pub mod torrent_manager; +// pub mod torrent_manager; pub mod torrent_state; pub mod tracker_comms; pub mod type_aliases; diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 644cb81..ddc213d 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -17,43 +17,28 @@ use crate::{ dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult}, peer_connection::PeerConnectionOptions, spawn_utils::{spawn, BlockingSpawner}, - torrent_manager::{TorrentManagerBuilder, TorrentManagerHandle}, + torrent_state::{ManagedTorrent, ManagedTorrentBuilder, ManagedTorrentHandle}, }; pub const SUPPORTED_SCHEMES: [&str; 3] = ["http:", "https:", "magnet:"]; -#[derive(Clone)] -pub enum ManagedTorrentState { - Initializing, - Running(TorrentManagerHandle), -} - -#[derive(Clone)] -pub struct ManagedTorrent { - pub info_hash: Id20, - pub output_folder: PathBuf, - pub state: ManagedTorrentState, -} - -impl PartialEq for ManagedTorrent { - fn eq(&self, other: &Self) -> bool { - self.info_hash == other.info_hash && self.output_folder == other.output_folder - } -} - #[derive(Default)] pub struct SessionLocked { - torrents: Vec, + torrents: Vec, } enum SessionLockedAddTorrentResult { - AlreadyManaged(ManagedTorrent), + AlreadyManaged(ManagedTorrentHandle), Added(usize), } impl SessionLocked { - fn add_torrent(&mut self, torrent: ManagedTorrent) -> SessionLockedAddTorrentResult { - if let Some(handle) = self.torrents.iter().find(|t| **t == torrent) { + fn add_torrent(&mut self, torrent: ManagedTorrentHandle) -> SessionLockedAddTorrentResult { + if let Some(handle) = self + .torrents + .iter() + .find(|t| t.info_hash() == torrent.info_hash()) + { return SessionLockedAddTorrentResult::AlreadyManaged(handle.clone()); } let idx = self.torrents.len(); @@ -124,9 +109,9 @@ pub struct ListOnlyResponse { } pub enum AddTorrentResponse { - AlreadyManaged(ManagedTorrent), + AlreadyManaged(ManagedTorrentHandle), ListOnly(ListOnlyResponse), - Added(TorrentManagerHandle), + Added(ManagedTorrentHandle), } pub fn read_local_file_including_stdin(filename: &str) -> anyhow::Result> { @@ -227,7 +212,7 @@ impl Session { } pub fn with_torrents(&self, callback: F) where - F: Fn(&[ManagedTorrent]), + F: Fn(&[ManagedTorrentHandle]), { callback(&self.locked.read().torrents) } @@ -404,24 +389,13 @@ impl Session { .unwrap_or_else(|| self.output_folder.clone()) .join(sub_folder); - let managed_torrent = ManagedTorrent { - info_hash, - output_folder: output_folder.clone(), - state: ManagedTorrentState::Initializing, - }; - - match self.locked.write().add_torrent(managed_torrent) { - SessionLockedAddTorrentResult::AlreadyManaged(managed) => { - return Ok(AddTorrentResponse::AlreadyManaged(managed)) - } - SessionLockedAddTorrentResult::Added(_) => {} - } - - let mut builder = TorrentManagerBuilder::new(info, info_hash, output_folder.clone()); + let mut builder = ManagedTorrentBuilder::new(info, info_hash, output_folder.clone()); builder .overwrite(opts.overwrite) .spawner(self.spawner) - .peer_id(self.peer_id); + .peer_id(self.peer_id) + .trackers(trackers); + if let Some(only_files) = only_files { builder.only_files(only_files); } @@ -437,51 +411,22 @@ impl Session { builder.peer_read_write_timeout(t); } - let handle = match builder - .start_manager() - .context("error starting torrent manager") - { - Ok(handle) => { - let mut g = self.locked.write(); - let m = g - .torrents - .iter_mut() - .find(|t| t.info_hash == info_hash && t.output_folder == output_folder) - .unwrap(); - m.state = ManagedTorrentState::Running(handle.clone()); - handle + let managed_torrent = builder.build(); + + match self.locked.write().add_torrent(managed_torrent.clone()) { + SessionLockedAddTorrentResult::AlreadyManaged(managed) => { + return Ok(AddTorrentResponse::AlreadyManaged(managed)) } - Err(error) => { - let mut g = self.locked.write(); - let idx = g - .torrents - .iter() - .position(|t| t.info_hash == info_hash && t.output_folder == output_folder) - .unwrap(); - g.torrents.remove(idx); - return Err(error); - } - }; - { - let mut g = self.locked.write(); - let m = g - .torrents - .iter_mut() - .find(|t| t.info_hash == info_hash && t.output_folder == output_folder) - .unwrap(); - m.state = ManagedTorrentState::Running(handle.clone()); + SessionLockedAddTorrentResult::Added(_) => {} } - for url in trackers { - handle.add_tracker(url); - } for peer in initial_peers { - handle.add_peer(peer); + managed_torrent.add_peer(peer); } if let Some(mut dht_peer_rx) = dht_peer_rx { spawn(span!(Level::INFO, "dht_peer_adder"), { - let handle = handle.clone(); + let handle = managed_torrent.clone(); async move { while let Some(peer) = dht_peer_rx.next().await { handle.add_peer(peer); @@ -492,6 +437,6 @@ impl Session { }); } - Ok(AddTorrentResponse::Added(handle)) + Ok(AddTorrentResponse::Added(managed_torrent)) } } diff --git a/crates/librqbit/src/torrent_manager.rs b/crates/librqbit/src/torrent_manager.rs index 4be9c9c..16507af 100644 --- a/crates/librqbit/src/torrent_manager.rs +++ b/crates/librqbit/src/torrent_manager.rs @@ -24,128 +24,10 @@ use crate::{ chunk_tracker::ChunkTracker, file_ops::FileOps, spawn_utils::{spawn, BlockingSpawner}, - torrent_state::{TorrentStateLive, TorrentStateOptions}, + torrent_state::{ManagedTorrent, ManagedTorrentHandle, TorrentStateLive, TorrentStateOptions}, tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, }; -#[derive(Default)] -struct TorrentManagerOptions { - force_tracker_interval: Option, - peer_connect_timeout: Option, - peer_read_write_timeout: Option, - only_files: Option>, - peer_id: Option, - overwrite: bool, -} - -pub struct TorrentManagerBuilder { - info: TorrentMetaV1Info, - info_hash: Id20, - output_folder: PathBuf, - options: TorrentManagerOptions, - spawner: Option, -} - -impl TorrentManagerBuilder { - pub fn new>( - info: TorrentMetaV1Info, - info_hash: Id20, - output_folder: P, - ) -> Self { - Self { - info, - info_hash, - output_folder: output_folder.as_ref().into(), - spawner: None, - options: TorrentManagerOptions::default(), - } - } - - pub fn only_files(&mut self, only_files: Vec) -> &mut Self { - self.options.only_files = Some(only_files); - self - } - - pub fn overwrite(&mut self, overwrite: bool) -> &mut Self { - self.options.overwrite = overwrite; - self - } - - pub fn force_tracker_interval(&mut self, force_tracker_interval: Duration) -> &mut Self { - self.options.force_tracker_interval = Some(force_tracker_interval); - self - } - - pub fn spawner(&mut self, spawner: BlockingSpawner) -> &mut Self { - self.spawner = Some(spawner); - self - } - - pub fn peer_id(&mut self, peer_id: Id20) -> &mut Self { - self.options.peer_id = Some(peer_id); - self - } - - pub fn peer_connect_timeout(&mut self, timeout: Duration) -> &mut Self { - self.options.peer_connect_timeout = Some(timeout); - self - } - - pub fn peer_read_write_timeout(&mut self, timeout: Duration) -> &mut Self { - self.options.peer_read_write_timeout = Some(timeout); - self - } - - pub fn start_manager(self) -> anyhow::Result { - TorrentManager::start( - self.info, - self.info_hash, - self.output_folder, - self.spawner.unwrap_or_else(|| BlockingSpawner::new(true)), - Some(self.options), - ) - } -} - -#[derive(Clone)] -pub struct TorrentManagerHandle { - manager: Arc, -} - -impl TorrentManagerHandle { - pub fn add_tracker(&self, url: Url) -> bool { - let mgr = self.manager.clone(); - if mgr.trackers.lock().insert(url.clone()) { - spawn( - span!(Level::ERROR, "tracker_monitor", url = url.to_string()), - async move { mgr.single_tracker_monitor(url).await }, - ); - true - } else { - false - } - } - pub fn only_files(&self) -> Option<&[usize]> { - self.manager.options.only_files.as_deref() - } - pub fn add_peer(&self, addr: SocketAddr) -> bool { - self.manager.state.add_peer_if_not_seen(addr) - } - pub fn torrent_state(&self) -> &TorrentStateLive { - &self.manager.state - } - pub fn speed_estimator(&self) -> &Arc { - &self.manager.speed_estimator - } - pub async fn cancel(&self) -> anyhow::Result<()> { - todo!() - } - pub async fn wait_until_completed(&self) -> anyhow::Result<()> { - self.manager.state.wait_until_completed().await; - Ok(()) - } -} - struct TorrentManager { state: Arc, #[allow(dead_code)] @@ -171,8 +53,8 @@ impl TorrentManager { info_hash: Id20, out: P, spawner: BlockingSpawner, - options: Option, - ) -> anyhow::Result { + options: Option, + ) -> anyhow::Result { let options = options.unwrap_or_default(); let (files, filenames) = { let mut files = diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 6335f6f..08650a5 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -63,6 +63,7 @@ use futures::{stream::FuturesUnordered, StreamExt}; use librqbit_core::{ id20::Id20, lengths::{ChunkInfo, Lengths, ValidPieceIndex}, + speed_estimator::{self, SpeedEstimator}, torrent_metainfo::TorrentMetaV1Info, }; use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -144,6 +145,8 @@ pub struct TorrentStateLive { peer_queue_tx: UnboundedSender, finished_notify: Notify, + + speed_estimator: SpeedEstimator, } impl TorrentStateLive { @@ -163,6 +166,9 @@ impl TorrentStateLive { ) -> Arc { let options = options.unwrap_or_default(); let (peer_queue_tx, peer_queue_rx) = unbounded_channel(); + + let speed_estimator = SpeedEstimator::new(5); + let state = Arc::new(TorrentStateLive { info_hash, info, @@ -186,6 +192,7 @@ impl TorrentStateLive { peer_semaphore: Semaphore::new(128), peer_queue_tx, finished_notify: Notify::new(), + speed_estimator, }); spawn( span!(Level::ERROR, "peer_adder"), @@ -194,6 +201,10 @@ impl TorrentStateLive { state } + pub fn speed_estimator(&self) -> &SpeedEstimator { + &self.speed_estimator + } + async fn task_manage_peer( self: Arc, addr: SocketAddr, diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 6b8347b..f566f51 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -2,36 +2,188 @@ pub mod utils; pub mod live; +use std::net::SocketAddr; +use std::path::PathBuf; use std::sync::Arc; +use std::time::Duration; +use std::{collections::HashSet, path::Path}; +use anyhow::Context; use buffers::ByteString; use librqbit_core::id20::Id20; +use librqbit_core::speed_estimator::SpeedEstimator; use librqbit_core::torrent_metainfo::TorrentMetaV1Info; pub use live::*; use parking_lot::RwLock; use tokio::sync::mpsc::Sender; +use tracing::trace_span; use url::Url; -pub(crate) enum ManagedTorrentState { - Live { - state: TorrentStateLive, - only_files_tx: Sender>, - trackers_tx: Sender, - }, +use crate::spawn_utils::{spawn, BlockingSpawner}; + +pub struct TorrentStateInitializing {} + +#[derive(Default, Clone)] +pub enum ManagedTorrentState { + #[default] + Created, + + Initializing(Arc), + + // TODO: only_files_tx + // TODO: trackers_tx?? + Live(Arc), } pub(crate) struct ManagedTorrentLocked { - pub trackers: Vec, - pub only_files: Vec, + pub only_files: Option>, pub state: ManagedTorrentState, } pub struct ManagedTorrentInfo { pub info: TorrentMetaV1Info, pub info_hash: Id20, + pub out_dir: PathBuf, + pub spawner: BlockingSpawner, + pub trackers: Vec, + // pub options: Option, } -pub(crate) struct ManagedTorrent { +pub struct ManagedTorrent { pub info: Arc, - pub(crate) locked: RwLock, + locked: RwLock, } + +impl ManagedTorrent { + pub fn info(&self) -> &ManagedTorrentInfo { + &self.info + } + + pub fn info_hash(&self) -> Id20 { + self.info.info_hash + } + + pub(crate) fn add_peer(&self, peer: SocketAddr) -> bool { + todo!() + } + + pub fn only_files(&self) -> Option> { + self.locked.write().only_files.clone() + } + + pub fn state(&self) -> ManagedTorrentState { + self.locked.read().state.clone() + } + + pub fn live(&self) -> Option> { + let g = self.locked.read(); + match &g.state { + ManagedTorrentState::Live(live) => Some(live.clone()), + _ => None, + } + } + + pub async fn wait_until_completed(&self) -> anyhow::Result<()> { + // TODO: rewrite + self.live() + .context("torrent isn't live")? + .wait_until_completed() + .await; + Ok(()) + } +} + +pub struct ManagedTorrentBuilder { + info: TorrentMetaV1Info, + info_hash: Id20, + output_folder: PathBuf, + force_tracker_interval: Option, + peer_connect_timeout: Option, + peer_read_write_timeout: Option, + only_files: Option>, + trackers: Vec, + peer_id: Option, + overwrite: bool, + spawner: Option, +} + +impl ManagedTorrentBuilder { + pub fn new>( + info: TorrentMetaV1Info, + info_hash: Id20, + output_folder: P, + ) -> Self { + Self { + info, + info_hash, + output_folder: output_folder.as_ref().into(), + spawner: None, + force_tracker_interval: None, + peer_connect_timeout: None, + peer_read_write_timeout: None, + only_files: None, + trackers: Default::default(), + peer_id: None, + overwrite: false, + } + } + + pub fn only_files(&mut self, only_files: Vec) -> &mut Self { + self.only_files = Some(only_files); + self + } + + pub fn trackers(&mut self, trackers: Vec) -> &mut Self { + self.trackers = trackers; + self + } + + pub fn overwrite(&mut self, overwrite: bool) -> &mut Self { + self.overwrite = overwrite; + self + } + + pub fn force_tracker_interval(&mut self, force_tracker_interval: Duration) -> &mut Self { + self.force_tracker_interval = Some(force_tracker_interval); + self + } + + pub fn spawner(&mut self, spawner: BlockingSpawner) -> &mut Self { + self.spawner = Some(spawner); + self + } + + pub fn peer_id(&mut self, peer_id: Id20) -> &mut Self { + self.peer_id = Some(peer_id); + self + } + + pub fn peer_connect_timeout(&mut self, timeout: Duration) -> &mut Self { + self.peer_connect_timeout = Some(timeout); + self + } + + pub fn peer_read_write_timeout(&mut self, timeout: Duration) -> &mut Self { + self.peer_read_write_timeout = Some(timeout); + self + } + + pub(crate) fn build(self) -> ManagedTorrentHandle { + Arc::new(ManagedTorrent { + locked: RwLock::new(ManagedTorrentLocked { + only_files: self.only_files, + state: Default::default(), + }), + info: Arc::new(ManagedTorrentInfo { + info: self.info, + info_hash: self.info_hash, + out_dir: self.output_folder, + trackers: self.trackers.into_iter().collect(), + spawner: self.spawner.unwrap_or_default(), + // options: Some(self.options), + }), + }) + } +} + +pub type ManagedTorrentHandle = Arc; diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index e457e36..8a94abe 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -7,10 +7,11 @@ use librqbit::{ http_api_client, peer_connection::PeerConnectionOptions, session::{ - AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, ManagedTorrentState, - Session, SessionOptions, + AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, + SessionOptions, }, spawn_utils::{spawn, BlockingSpawner}, + torrent_state::ManagedTorrentState, }; use size_format::SizeFormatterBinary as SF; use tracing::{error, info, span, warn, Level}; @@ -238,12 +239,12 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> loop { session.with_torrents(|torrents| { for (idx, torrent) in torrents.iter().enumerate() { - match &torrent.state { - ManagedTorrentState::Initializing => { + match torrent.state() { + ManagedTorrentState::Initializing(_) => { info!("[{}] initializing", idx); }, - ManagedTorrentState::Running(handle) => { - let stats = handle.torrent_state().stats_snapshot(); + ManagedTorrentState::Live(handle) => { + let stats = handle.stats_snapshot(); let speed = handle.speed_estimator(); let total = stats.total_bytes; let progress = stats.total_bytes - stats.remaining_bytes; @@ -269,6 +270,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> stats.peer_stats.dead, ); }, + ManagedTorrentState::Created => warn!("the torrent was just created, but not initializing"), } } }); @@ -394,7 +396,8 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> AddTorrentResponse::AlreadyManaged(handle) => { info!( "torrent {:?} is already managed, downloaded to {:?}", - handle.info_hash, handle.output_folder + handle.info_hash(), + handle.info().out_dir ); continue; } From d8538af25d7295acd5ef24469a4c80bf11c62114 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 12:44:36 +0000 Subject: [PATCH 08/38] [2/n] HUGE REFACTOR to suppor multiple states. Incomplete, broken --- crates/librqbit/src/chunk_tracker.rs | 12 + crates/librqbit/src/torrent_manager.rs | 262 ------------------ .../src/torrent_state/initializing.rs | 157 +++++++++++ crates/librqbit/src/torrent_state/live/mod.rs | 181 ++++++++---- crates/librqbit/src/torrent_state/mod.rs | 67 +++-- crates/librqbit/src/torrent_state/paused.rs | 14 + crates/rqbit/src/main.rs | 70 ++--- 7 files changed, 394 insertions(+), 369 deletions(-) create mode 100644 crates/librqbit/src/torrent_state/initializing.rs create mode 100644 crates/librqbit/src/torrent_state/paused.rs diff --git a/crates/librqbit/src/chunk_tracker.rs b/crates/librqbit/src/chunk_tracker.rs index 880ce53..f5b16e3 100644 --- a/crates/librqbit/src/chunk_tracker.rs +++ b/crates/librqbit/src/chunk_tracker.rs @@ -76,6 +76,18 @@ impl ChunkTracker { } } + pub fn get_have_bytes(&self) -> u64 { + todo!() + } + + pub fn get_needed_bytes(&self) -> u64 { + todo!() + } + + pub fn get_lengths(&self) -> &Lengths { + &self.lengths + } + pub fn get_have_pieces(&self) -> &BF { &self.have } diff --git a/crates/librqbit/src/torrent_manager.rs b/crates/librqbit/src/torrent_manager.rs index 16507af..e69de29 100644 --- a/crates/librqbit/src/torrent_manager.rs +++ b/crates/librqbit/src/torrent_manager.rs @@ -1,262 +0,0 @@ -use std::{ - collections::HashSet, - fs::{File, OpenOptions}, - net::SocketAddr, - path::{Path, PathBuf}, - sync::Arc, - time::{Duration, Instant}, -}; - -use anyhow::Context; -use bencode::from_bytes; -use buffers::ByteString; -use librqbit_core::{ - id20::Id20, lengths::Lengths, peer_id::generate_peer_id, speed_estimator::SpeedEstimator, - torrent_metainfo::TorrentMetaV1Info, -}; -use parking_lot::Mutex; -use reqwest::Url; -use sha1w::Sha1; -use size_format::SizeFormatterBinary as SF; -use tracing::{debug, info, span, warn, Level}; - -use crate::{ - chunk_tracker::ChunkTracker, - file_ops::FileOps, - spawn_utils::{spawn, BlockingSpawner}, - torrent_state::{ManagedTorrent, ManagedTorrentHandle, TorrentStateLive, TorrentStateOptions}, - tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, -}; - -struct TorrentManager { - state: Arc, - #[allow(dead_code)] - speed_estimator: Arc, - trackers: Mutex>, - options: TorrentManagerOptions, -} - -fn make_lengths>( - torrent: &TorrentMetaV1Info, -) -> anyhow::Result { - let total_length = torrent.iter_file_lengths()?.sum(); - Lengths::new(total_length, torrent.piece_length, None) -} - -fn ensure_file_length(file: &File, length: u64) -> anyhow::Result<()> { - Ok(file.set_len(length)?) -} - -impl TorrentManager { - fn start>( - info: TorrentMetaV1Info, - info_hash: Id20, - out: P, - spawner: BlockingSpawner, - options: Option, - ) -> anyhow::Result { - let options = options.unwrap_or_default(); - let (files, filenames) = { - let mut files = - Vec::>>::with_capacity(info.iter_file_lengths()?.count()); - let mut filenames = Vec::new(); - for (path_bits, _) in info.iter_filenames_and_lengths()? { - let mut full_path = out.as_ref().to_owned(); - let relative_path = path_bits - .to_pathbuf() - .context("error converting file to path")?; - full_path.push(relative_path); - - std::fs::create_dir_all(full_path.parent().unwrap())?; - let file = if options.overwrite { - OpenOptions::new() - .create(true) - .read(true) - .write(true) - .open(&full_path)? - } else { - // TODO: create_new does not seem to work with read(true), so calling this twice. - OpenOptions::new() - .create_new(true) - .write(true) - .open(&full_path) - .with_context(|| format!("error creating {:?}", &full_path))?; - OpenOptions::new().read(true).write(true).open(&full_path)? - }; - filenames.push(full_path); - files.push(Arc::new(Mutex::new(file))) - } - (files, filenames) - }; - - let peer_id = options.peer_id.unwrap_or_else(generate_peer_id); - let lengths = make_lengths(&info).context("unable to compute Lengths from torrent")?; - debug!("computed lengths: {:?}", &lengths); - - info!("Doing initial checksum validation, this might take a while..."); - let initial_check_results = spawner.spawn_block_in_place(|| { - FileOps::::new(&info, &files, &lengths) - .initial_check(options.only_files.as_deref()) - })?; - - info!( - "Initial check results: have {}, needed {}", - SF::new(initial_check_results.have_bytes), - SF::new(initial_check_results.needed_bytes) - ); - - spawner.spawn_block_in_place(|| { - for (idx, (file, (name, length))) in files - .iter() - .zip(info.iter_filenames_and_lengths().unwrap()) - .enumerate() - { - if options - .only_files - .as_ref() - .map(|v| !v.contains(&idx)) - .unwrap_or(false) - { - continue; - } - let now = Instant::now(); - if let Err(err) = ensure_file_length(&file.lock(), length) { - warn!( - "Error setting length for file {:?} to {}: {:#?}", - name, length, err - ); - } else { - debug!( - "Set length for file {:?} to {} in {:?}", - name, - SF::new(length), - now.elapsed() - ); - } - } - }); - - let chunk_tracker = ChunkTracker::new( - initial_check_results.needed_pieces, - initial_check_results.have_pieces, - lengths, - ); - - #[allow(clippy::needless_update)] - let state_options = TorrentStateOptions { - peer_connect_timeout: options.peer_connect_timeout, - peer_read_write_timeout: options.peer_read_write_timeout, - ..Default::default() - }; - - let state = TorrentStateLive::new( - info, - info_hash, - peer_id, - files, - filenames, - chunk_tracker, - lengths, - initial_check_results.have_bytes, - initial_check_results.needed_bytes, - spawner, - Some(state_options), - ); - - let estimator = Arc::new(SpeedEstimator::new(5)); - - let mgr = Arc::new(Self { - state, - speed_estimator: estimator.clone(), - trackers: Mutex::new(HashSet::new()), - options, - }); - - spawn(span!(Level::ERROR, "speed_estimator_updater"), { - let state = mgr.state.clone(); - async move { - loop { - let stats = state.stats_snapshot(); - let fetched = stats.fetched_bytes; - let needed = state.initially_needed(); - // fetched can be too high in theory, so for safety make sure that it doesn't wrap around u64. - let remaining = needed - .wrapping_sub(fetched) - .min(needed - stats.downloaded_and_checked_bytes); - estimator.add_snapshot(fetched, remaining, Instant::now()); - tokio::time::sleep(Duration::from_secs(1)).await; - } - } - }); - - Ok(mgr.into_handle()) - } - - fn into_handle(self: Arc) -> TorrentManagerHandle { - TorrentManagerHandle { manager: self } - } - - async fn tracker_one_request(&self, tracker_url: Url) -> anyhow::Result { - let response: reqwest::Response = reqwest::get(tracker_url).await?; - if !response.status().is_success() { - anyhow::bail!("tracker responded with {:?}", response.status()); - } - let bytes = response.bytes().await?; - if let Ok(error) = from_bytes::(&bytes) { - anyhow::bail!( - "tracker returned failure. Failure reason: {}", - error.failure_reason - ) - }; - let response = from_bytes::(&bytes)?; - - for peer in response.peers.iter_sockaddrs() { - self.state.add_peer_if_not_seen(peer); - } - Ok(response.interval) - } - - async fn single_tracker_monitor(&self, mut tracker_url: Url) -> anyhow::Result<()> { - let mut event = Some(TrackerRequestEvent::Started); - loop { - let request = TrackerRequest { - info_hash: self.state.info_hash(), - peer_id: self.state.peer_id(), - port: 6778, - uploaded: self.state.get_uploaded_bytes(), - downloaded: self.state.get_downloaded_bytes(), - left: self.state.get_left_to_download_bytes(), - compact: true, - no_peer_id: false, - event, - ip: None, - numwant: None, - key: None, - trackerid: None, - }; - - let request_query = request.as_querystring(); - tracker_url.set_query(Some(&request_query)); - - match self.tracker_one_request(tracker_url.clone()).await { - Ok(interval) => { - event = None; - let interval = self - .options - .force_tracker_interval - .unwrap_or_else(|| Duration::from_secs(interval)); - debug!( - "sleeping for {:?} after calling tracker {}", - interval, - tracker_url.host().unwrap() - ); - tokio::time::sleep(interval).await; - } - Err(e) => { - debug!("error calling the tracker {}: {:#}", tracker_url, e); - tokio::time::sleep(Duration::from_secs(60)).await; - } - }; - } - } -} diff --git a/crates/librqbit/src/torrent_state/initializing.rs b/crates/librqbit/src/torrent_state/initializing.rs new file mode 100644 index 0000000..2873cb1 --- /dev/null +++ b/crates/librqbit/src/torrent_state/initializing.rs @@ -0,0 +1,157 @@ +use std::{ + collections::HashSet, + fs::{File, OpenOptions}, + net::SocketAddr, + path::{Path, PathBuf}, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::Context; +use bencode::from_bytes; +use buffers::ByteString; +use librqbit_core::{ + id20::Id20, lengths::Lengths, peer_id::generate_peer_id, speed_estimator::SpeedEstimator, + torrent_metainfo::TorrentMetaV1Info, +}; +use parking_lot::Mutex; +use reqwest::Url; +use sha1w::Sha1; +use size_format::SizeFormatterBinary as SF; +use tracing::{debug, info, span, warn, Level}; + +use crate::{ + chunk_tracker::ChunkTracker, + file_ops::FileOps, + spawn_utils::{spawn, BlockingSpawner}, + torrent_state::{ManagedTorrent, ManagedTorrentHandle, TorrentStateLive, TorrentStateOptions}, + tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, +}; + +use super::{paused::TorrentStatePaused, ManagedTorrentInfo}; + +fn make_lengths>( + torrent: &TorrentMetaV1Info, +) -> anyhow::Result { + let total_length = torrent.iter_file_lengths()?.sum(); + Lengths::new(total_length, torrent.piece_length, None) +} + +fn ensure_file_length(file: &File, length: u64) -> anyhow::Result<()> { + Ok(file.set_len(length)?) +} + +pub struct TorrentStateInitializing { + info: Arc, + only_files: Option>, +} + +impl TorrentStateInitializing { + pub fn new(info: Arc, only_files: Option>) -> Self { + Self { info, only_files } + } + + pub async fn check(&self) -> anyhow::Result { + let (files, filenames) = { + let mut files = Vec::>>::with_capacity( + (&self.info).info.iter_file_lengths()?.count(), + ); + let mut filenames = Vec::new(); + for (path_bits, _) in (&self.info).info.iter_filenames_and_lengths()? { + let mut full_path = (&self.info).out_dir.clone(); + let relative_path = path_bits + .to_pathbuf() + .context("error converting file to path")?; + full_path.push(relative_path); + + std::fs::create_dir_all(full_path.parent().unwrap())?; + let file = if (&self.info).options.overwrite { + OpenOptions::new() + .create(true) + .read(true) + .write(true) + .open(&full_path)? + } else { + // TODO: create_new does not seem to work with read(true), so calling this twice. + OpenOptions::new() + .create_new(true) + .write(true) + .open(&full_path) + .with_context(|| format!("error creating {:?}", &full_path))?; + OpenOptions::new().read(true).write(true).open(&full_path)? + }; + filenames.push(full_path); + files.push(Arc::new(Mutex::new(file))) + } + (files, filenames) + }; + + let lengths = + make_lengths(&(&self.info).info).context("unable to compute Lengths from torrent")?; + debug!("computed lengths: {:?}", &lengths); + + info!("Doing initial checksum validation, this might take a while..."); + let initial_check_results = (&self.info).spawner.spawn_block_in_place(|| { + FileOps::::new(&(&self.info).info, &files, &lengths) + .initial_check(self.only_files.as_deref()) + })?; + + info!( + "Initial check results: have {}, needed {}", + SF::new(initial_check_results.have_bytes), + SF::new(initial_check_results.needed_bytes) + ); + + (&self.info).spawner.spawn_block_in_place(|| { + for (idx, (file, (name, length))) in files + .iter() + .zip((&self.info).info.iter_filenames_and_lengths().unwrap()) + .enumerate() + { + if self + .only_files + .as_ref() + .map(|v| !v.contains(&idx)) + .unwrap_or(false) + { + continue; + } + let now = Instant::now(); + if let Err(err) = ensure_file_length(&file.lock(), length) { + warn!( + "Error setting length for file {:?} to {}: {:#?}", + name, length, err + ); + } else { + debug!( + "Set length for file {:?} to {} in {:?}", + name, + SF::new(length), + now.elapsed() + ); + } + } + }); + + let chunk_tracker = ChunkTracker::new( + initial_check_results.needed_pieces, + initial_check_results.have_pieces, + lengths, + ); + + #[allow(clippy::needless_update)] + let state_options = TorrentStateOptions { + peer_connect_timeout: (&self.info).options.peer_connect_timeout, + peer_read_write_timeout: (&self.info).options.peer_read_write_timeout, + ..Default::default() + }; + + let paused = TorrentStatePaused { + info: self.info.clone(), + files, + filenames, + chunk_tracker, + }; + Ok(paused) + } +} diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 08650a5..e59a6d3 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -57,12 +57,13 @@ use std::{ use anyhow::{bail, Context}; use backoff::backoff::Backoff; +use bencode::from_bytes; use buffers::{ByteBuf, ByteString}; use clone_to_owned::CloneToOwned; use futures::{stream::FuturesUnordered, StreamExt}; use librqbit_core::{ id20::Id20, - lengths::{ChunkInfo, Lengths, ValidPieceIndex}, + lengths::{self, ChunkInfo, Lengths, ValidPieceIndex}, speed_estimator::{self, SpeedEstimator}, torrent_metainfo::TorrentMetaV1Info, }; @@ -78,7 +79,8 @@ use tokio::{ }, time::timeout, }; -use tracing::{debug, error, info, span, trace, warn, Level}; +use tracing::{debug, error, info, span, trace, trace_span, warn, Level}; +use url::Url; use crate::{ chunk_tracker::{ChunkMarkingResult, ChunkTracker}, @@ -87,6 +89,7 @@ use crate::{ PeerConnection, PeerConnectionHandler, PeerConnectionOptions, WriterRequest, }, spawn_utils::{spawn, BlockingSpawner}, + tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, type_aliases::{PeerHandle, BF}, }; @@ -102,7 +105,11 @@ use self::{ stats::{atomic::AtomicStats, snapshot::StatsSnapshot}, }; -use super::utils::{timeit, TimedExistence}; +use super::{ + paused::TorrentStatePaused, + utils::{timeit, TimedExistence}, + ManagedTorrentInfo, +}; struct InflightPiece { peer: PeerHandle, @@ -126,17 +133,17 @@ pub struct TorrentStateOptions { pub struct TorrentStateLive { peers: PeerStates, - info: TorrentMetaV1Info, + meta: Arc, locked: Arc>, files: Vec>>, filenames: Vec, - info_hash: Id20, - peer_id: Id20, - lengths: Lengths, + + // TODO: why the hell do we need these here, remove it. needed_bytes: u64, have_plus_needed_bytes: u64, + stats: AtomicStats, - options: TorrentStateOptions, + lengths: Lengths, // Limits how many active (occupying network resources) peers there are at a moment in time. peer_semaphore: Semaphore, @@ -151,35 +158,24 @@ pub struct TorrentStateLive { impl TorrentStateLive { #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - info: TorrentMetaV1Info, - info_hash: Id20, - peer_id: Id20, - files: Vec>>, - filenames: Vec, - chunk_tracker: ChunkTracker, - lengths: Lengths, - have_bytes: u64, - needed_bytes: u64, - spawner: BlockingSpawner, - options: Option, - ) -> Arc { - let options = options.unwrap_or_default(); + pub(crate) fn new(paused: TorrentStatePaused) -> Arc { let (peer_queue_tx, peer_queue_rx) = unbounded_channel(); let speed_estimator = SpeedEstimator::new(5); + let have_bytes = paused.chunk_tracker.get_have_bytes(); + let needed_bytes = paused.chunk_tracker.get_needed_bytes(); + let lengths = paused.chunk_tracker.get_lengths().clone(); + let state = Arc::new(TorrentStateLive { - info_hash, - info, - peer_id, + meta: paused.info.clone(), peers: Default::default(), locked: Arc::new(RwLock::new(TorrentStateLocked { - chunks: chunk_tracker, + chunks: paused.chunk_tracker, inflight_pieces: Default::default(), })), - files, - filenames, + files: paused.files, + filenames: paused.filenames, stats: AtomicStats { have_bytes: AtomicU64::new(have_bytes), ..Default::default() @@ -187,16 +183,42 @@ impl TorrentStateLive { needed_bytes, have_plus_needed_bytes: needed_bytes + have_bytes, lengths, - options, peer_semaphore: Semaphore::new(128), peer_queue_tx, finished_notify: Notify::new(), speed_estimator, }); + + for tracker in state.meta.trackers.iter() { + spawn( + trace_span!("tracker_monitor", url = tracker.to_string()), + state.clone().task_single_tracker_monitor(tracker.clone()), + ); + } + + spawn(span!(Level::ERROR, "speed_estimator_updater"), { + let state = state.clone(); + async move { + loop { + let stats = state.stats_snapshot(); + let fetched = stats.fetched_bytes; + let needed = state.initially_needed(); + // fetched can be too high in theory, so for safety make sure that it doesn't wrap around u64. + let remaining = needed + .wrapping_sub(fetched) + .min(needed - stats.downloaded_and_checked_bytes); + state + .speed_estimator + .add_snapshot(fetched, remaining, Instant::now()); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + }); + spawn( span!(Level::ERROR, "peer_adder"), - state.clone().task_peer_adder(peer_queue_rx, spawner), + state.clone().task_peer_adder(peer_queue_rx), ); state } @@ -205,11 +227,75 @@ impl TorrentStateLive { &self.speed_estimator } - async fn task_manage_peer( + async fn tracker_one_request(&self, tracker_url: Url) -> anyhow::Result { + let response: reqwest::Response = reqwest::get(tracker_url).await?; + if !response.status().is_success() { + anyhow::bail!("tracker responded with {:?}", response.status()); + } + let bytes = response.bytes().await?; + if let Ok(error) = from_bytes::(&bytes) { + anyhow::bail!( + "tracker returned failure. Failure reason: {}", + error.failure_reason + ) + }; + let response = from_bytes::(&bytes)?; + + for peer in response.peers.iter_sockaddrs() { + self.add_peer_if_not_seen(peer); + } + Ok(response.interval) + } + + async fn task_single_tracker_monitor( self: Arc, - addr: SocketAddr, - spawner: BlockingSpawner, + mut tracker_url: Url, ) -> anyhow::Result<()> { + let mut event = Some(TrackerRequestEvent::Started); + loop { + let request = TrackerRequest { + info_hash: self.info_hash(), + peer_id: self.peer_id(), + port: 6778, + uploaded: self.get_uploaded_bytes(), + downloaded: self.get_downloaded_bytes(), + left: self.get_left_to_download_bytes(), + compact: true, + no_peer_id: false, + event, + ip: None, + numwant: None, + key: None, + trackerid: None, + }; + + let request_query = request.as_querystring(); + tracker_url.set_query(Some(&request_query)); + + match self.tracker_one_request(tracker_url.clone()).await { + Ok(interval) => { + event = None; + let interval = self + .meta + .options + .force_tracker_interval + .unwrap_or_else(|| Duration::from_secs(interval)); + debug!( + "sleeping for {:?} after calling tracker {}", + interval, + tracker_url.host().unwrap() + ); + tokio::time::sleep(interval).await; + } + Err(e) => { + debug!("error calling the tracker {}: {:#}", tracker_url, e); + tokio::time::sleep(Duration::from_secs(60)).await; + } + }; + } + } + + async fn task_manage_peer(self: Arc, addr: SocketAddr) -> anyhow::Result<()> { let state = self; let (rx, tx) = state.peers.mark_peer_connecting(addr)?; @@ -229,21 +315,20 @@ impl TorrentStateLive { requests_sem: Semaphore::new(0), state: state.clone(), tx, - spawner, counters, }; let options = PeerConnectionOptions { - connect_timeout: state.options.peer_connect_timeout, - read_write_timeout: state.options.peer_read_write_timeout, + connect_timeout: state.meta.options.peer_connect_timeout, + read_write_timeout: state.meta.options.peer_read_write_timeout, ..Default::default() }; let peer_connection = PeerConnection::new( addr, - state.info_hash, - state.peer_id, + state.meta.info_hash, + state.meta.peer_id, &handler, Some(options), - spawner, + state.meta.spawner, ); let requester = handler.task_peer_chunk_requester(addr); @@ -274,7 +359,6 @@ impl TorrentStateLive { async fn task_peer_adder( self: Arc, mut peer_queue_rx: UnboundedReceiver, - spawner: BlockingSpawner, ) -> anyhow::Result<()> { let state = self; loop { @@ -289,22 +373,22 @@ impl TorrentStateLive { permit.forget(); spawn( span!(parent: None, Level::ERROR, "manage_peer", peer = addr.to_string()), - state.clone().task_manage_peer(addr, spawner), + state.clone().task_manage_peer(addr), ); } } pub fn info(&self) -> &TorrentMetaV1Info { - &self.info + &self.meta.info } pub fn info_hash(&self) -> Id20 { - self.info_hash + self.meta.info_hash } pub fn peer_id(&self) -> Id20 { - self.peer_id + self.meta.peer_id } pub(crate) fn file_ops(&self) -> FileOps<'_, Sha1> { - FileOps::new(&self.info, &self.files, &self.lengths) + FileOps::new(&self.meta.info, &self.files, &self.lengths) } pub fn initially_needed(&self) -> u64 { self.needed_bytes @@ -429,7 +513,7 @@ impl TorrentStateLive { ); } - pub(crate) fn add_peer_if_not_seen(self: &Arc, addr: SocketAddr) -> bool { + pub(crate) fn add_peer_if_not_seen(&self, addr: SocketAddr) -> bool { match self.peers.add_if_not_seen(addr) { Some(handle) => handle, None => return false, @@ -509,7 +593,6 @@ struct PeerHandler { requests_sem: Semaphore, addr: SocketAddr, - spawner: BlockingSpawner, tx: PeerTx, } @@ -1083,7 +1166,9 @@ impl PeerHandler { // By this time we reach here, no other peer can for this piece. All others, even if they steal pieces would // have fallen off above in one of the defensive checks. - self.spawner + self.state + .meta + .spawner .spawn_block_in_place(move || { let index = piece.index; diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index f566f51..eda146a 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -1,6 +1,7 @@ -pub mod utils; - +pub mod initializing; pub mod live; +pub mod paused; +pub mod utils; use std::net::SocketAddr; use std::path::PathBuf; @@ -11,6 +12,7 @@ use std::{collections::HashSet, path::Path}; use anyhow::Context; use buffers::ByteString; use librqbit_core::id20::Id20; +use librqbit_core::peer_id::generate_peer_id; use librqbit_core::speed_estimator::SpeedEstimator; use librqbit_core::torrent_metainfo::TorrentMetaV1Info; pub use live::*; @@ -21,32 +23,37 @@ use url::Url; use crate::spawn_utils::{spawn, BlockingSpawner}; -pub struct TorrentStateInitializing {} +use initializing::TorrentStateInitializing; + +use self::paused::TorrentStatePaused; -#[derive(Default, Clone)] pub enum ManagedTorrentState { - #[default] - Created, - - Initializing(Arc), - - // TODO: only_files_tx - // TODO: trackers_tx?? + Initializing(TorrentStateInitializing), + Paused(TorrentStatePaused), Live(Arc), + Error(anyhow::Error), } pub(crate) struct ManagedTorrentLocked { - pub only_files: Option>, pub state: ManagedTorrentState, } +#[derive(Default)] +pub(crate) struct ManagedTorrentOptions { + pub force_tracker_interval: Option, + pub peer_connect_timeout: Option, + pub peer_read_write_timeout: Option, + pub overwrite: bool, +} + pub struct ManagedTorrentInfo { pub info: TorrentMetaV1Info, pub info_hash: Id20, pub out_dir: PathBuf, pub spawner: BlockingSpawner, pub trackers: Vec, - // pub options: Option, + pub peer_id: Id20, + pub(crate) options: ManagedTorrentOptions, } pub struct ManagedTorrent { @@ -68,11 +75,12 @@ impl ManagedTorrent { } pub fn only_files(&self) -> Option> { - self.locked.write().only_files.clone() + // self.locked.write().only_files.clone() + todo!() } - pub fn state(&self) -> ManagedTorrentState { - self.locked.read().state.clone() + pub fn with_state(&self, f: impl FnOnce(&ManagedTorrentState) -> R) -> R { + f(&self.locked.read().state) } pub fn live(&self) -> Option> { @@ -169,19 +177,26 @@ impl ManagedTorrentBuilder { } pub(crate) fn build(self) -> ManagedTorrentHandle { + let info = Arc::new(ManagedTorrentInfo { + info: self.info, + info_hash: self.info_hash, + out_dir: self.output_folder, + trackers: self.trackers.into_iter().collect(), + spawner: self.spawner.unwrap_or_default(), + peer_id: self.peer_id.unwrap_or_else(generate_peer_id), + options: ManagedTorrentOptions { + force_tracker_interval: self.force_tracker_interval, + peer_connect_timeout: self.peer_connect_timeout, + peer_read_write_timeout: self.peer_read_write_timeout, + overwrite: self.overwrite, + }, + }); + let initializing = TorrentStateInitializing::new(info.clone(), self.only_files); Arc::new(ManagedTorrent { locked: RwLock::new(ManagedTorrentLocked { - only_files: self.only_files, - state: Default::default(), - }), - info: Arc::new(ManagedTorrentInfo { - info: self.info, - info_hash: self.info_hash, - out_dir: self.output_folder, - trackers: self.trackers.into_iter().collect(), - spawner: self.spawner.unwrap_or_default(), - // options: Some(self.options), + state: ManagedTorrentState::Initializing(initializing), }), + info, }) } } diff --git a/crates/librqbit/src/torrent_state/paused.rs b/crates/librqbit/src/torrent_state/paused.rs new file mode 100644 index 0000000..d4f9da7 --- /dev/null +++ b/crates/librqbit/src/torrent_state/paused.rs @@ -0,0 +1,14 @@ +use std::{fs::File, path::PathBuf, sync::Arc}; + +use parking_lot::Mutex; + +use crate::chunk_tracker::ChunkTracker; + +use super::ManagedTorrentInfo; + +pub struct TorrentStatePaused { + pub(crate) info: Arc, + pub(crate) files: Vec>>, + pub(crate) filenames: Vec, + pub(crate) chunk_tracker: ChunkTracker, +} diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 8a94abe..c46d0e2 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -239,39 +239,43 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> loop { session.with_torrents(|torrents| { for (idx, torrent) in torrents.iter().enumerate() { - match torrent.state() { - ManagedTorrentState::Initializing(_) => { - info!("[{}] initializing", idx); - }, - ManagedTorrentState::Live(handle) => { - let stats = handle.stats_snapshot(); - let speed = handle.speed_estimator(); - let total = stats.total_bytes; - let progress = stats.total_bytes - stats.remaining_bytes; - let downloaded_pct = if stats.remaining_bytes == 0 { - 100f64 - } else { - (progress as f64 / total as f64) * 100f64 - }; - info!( - "[{}]: {:.2}% ({:.2}), down speed {:.2} MiB/s, fetched {}, remaining {:.2} of {:.2}, uploaded {:.2}, peers: {{live: {}, connecting: {}, queued: {}, seen: {}, dead: {}}}", - idx, - downloaded_pct, - SF::new(progress), - speed.download_mbps(), - SF::new(stats.fetched_bytes), - SF::new(stats.remaining_bytes), - SF::new(total), - SF::new(stats.uploaded_bytes), - stats.peer_stats.live, - stats.peer_stats.connecting, - stats.peer_stats.queued, - stats.peer_stats.seen, - stats.peer_stats.dead, - ); - }, - ManagedTorrentState::Created => warn!("the torrent was just created, but not initializing"), - } + let live = torrent.with_state(|s| { + match s { + ManagedTorrentState::Initializing(_) => info!("[{}] initializing", idx), + ManagedTorrentState::Live(h) => return Some(h.clone()), + ManagedTorrentState::Error(_) | ManagedTorrentState::Paused(_) => {}, + }; + None + }); + let handle = match live { + Some(live) => live, + None => continue + }; + let stats = handle.stats_snapshot(); + let speed = handle.speed_estimator(); + let total = stats.total_bytes; + let progress = stats.total_bytes - stats.remaining_bytes; + let downloaded_pct = if stats.remaining_bytes == 0 { + 100f64 + } else { + (progress as f64 / total as f64) * 100f64 + }; + info!( + "[{}]: {:.2}% ({:.2}), down speed {:.2} MiB/s, fetched {}, remaining {:.2} of {:.2}, uploaded {:.2}, peers: {{live: {}, connecting: {}, queued: {}, seen: {}, dead: {}}}", + idx, + downloaded_pct, + SF::new(progress), + speed.download_mbps(), + SF::new(stats.fetched_bytes), + SF::new(stats.remaining_bytes), + SF::new(total), + SF::new(stats.uploaded_bytes), + stats.peer_stats.live, + stats.peer_stats.connecting, + stats.peer_stats.queued, + stats.peer_stats.seen, + stats.peer_stats.dead, + ); } }); tokio::time::sleep(Duration::from_secs(1)).await; From 0aa1af79336b83b1bf36110d17e3516665b16cf8 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 12:47:17 +0000 Subject: [PATCH 09/38] saving --- .../src/torrent_state/initializing.rs | 38 ++++++++----------- crates/librqbit/src/torrent_state/paused.rs | 2 + 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/crates/librqbit/src/torrent_state/initializing.rs b/crates/librqbit/src/torrent_state/initializing.rs index 2873cb1..8d8bc02 100644 --- a/crates/librqbit/src/torrent_state/initializing.rs +++ b/crates/librqbit/src/torrent_state/initializing.rs @@ -42,30 +42,29 @@ fn ensure_file_length(file: &File, length: u64) -> anyhow::Result<()> { } pub struct TorrentStateInitializing { - info: Arc, + meta: Arc, only_files: Option>, } impl TorrentStateInitializing { - pub fn new(info: Arc, only_files: Option>) -> Self { - Self { info, only_files } + pub fn new(meta: Arc, only_files: Option>) -> Self { + Self { meta, only_files } } pub async fn check(&self) -> anyhow::Result { let (files, filenames) = { - let mut files = Vec::>>::with_capacity( - (&self.info).info.iter_file_lengths()?.count(), - ); + let mut files = + Vec::>>::with_capacity(self.meta.info.iter_file_lengths()?.count()); let mut filenames = Vec::new(); - for (path_bits, _) in (&self.info).info.iter_filenames_and_lengths()? { - let mut full_path = (&self.info).out_dir.clone(); + for (path_bits, _) in self.meta.info.iter_filenames_and_lengths()? { + let mut full_path = self.meta.out_dir.clone(); let relative_path = path_bits .to_pathbuf() .context("error converting file to path")?; full_path.push(relative_path); std::fs::create_dir_all(full_path.parent().unwrap())?; - let file = if (&self.info).options.overwrite { + let file = if (&self.meta).options.overwrite { OpenOptions::new() .create(true) .read(true) @@ -87,12 +86,12 @@ impl TorrentStateInitializing { }; let lengths = - make_lengths(&(&self.info).info).context("unable to compute Lengths from torrent")?; + make_lengths(&(&self.meta).info).context("unable to compute Lengths from torrent")?; debug!("computed lengths: {:?}", &lengths); info!("Doing initial checksum validation, this might take a while..."); - let initial_check_results = (&self.info).spawner.spawn_block_in_place(|| { - FileOps::::new(&(&self.info).info, &files, &lengths) + let initial_check_results = (&self.meta).spawner.spawn_block_in_place(|| { + FileOps::::new(&(&self.meta).info, &files, &lengths) .initial_check(self.only_files.as_deref()) })?; @@ -102,10 +101,10 @@ impl TorrentStateInitializing { SF::new(initial_check_results.needed_bytes) ); - (&self.info).spawner.spawn_block_in_place(|| { + self.meta.spawner.spawn_block_in_place(|| { for (idx, (file, (name, length))) in files .iter() - .zip((&self.info).info.iter_filenames_and_lengths().unwrap()) + .zip(self.meta.info.iter_filenames_and_lengths().unwrap()) .enumerate() { if self @@ -139,18 +138,13 @@ impl TorrentStateInitializing { lengths, ); - #[allow(clippy::needless_update)] - let state_options = TorrentStateOptions { - peer_connect_timeout: (&self.info).options.peer_connect_timeout, - peer_read_write_timeout: (&self.info).options.peer_read_write_timeout, - ..Default::default() - }; - let paused = TorrentStatePaused { - info: self.info.clone(), + info: self.meta.clone(), files, filenames, chunk_tracker, + have_bytes: initial_check_results.have_bytes, + needed_bytes: initial_check_results.needed_bytes, }; Ok(paused) } diff --git a/crates/librqbit/src/torrent_state/paused.rs b/crates/librqbit/src/torrent_state/paused.rs index d4f9da7..0a2b2c9 100644 --- a/crates/librqbit/src/torrent_state/paused.rs +++ b/crates/librqbit/src/torrent_state/paused.rs @@ -11,4 +11,6 @@ pub struct TorrentStatePaused { pub(crate) files: Vec>>, pub(crate) filenames: Vec, pub(crate) chunk_tracker: ChunkTracker, + pub(crate) have_bytes: u64, + pub(crate) needed_bytes: u64, } From 5e728fc67bf3b6486510e9fcd23444b2b744ed8c Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 12:47:33 +0000 Subject: [PATCH 10/38] saving --- crates/librqbit/src/http_api.rs | 4 +-- crates/librqbit/src/session.rs | 2 +- .../src/torrent_state/initializing.rs | 33 ++++++------------- crates/librqbit/src/torrent_state/live/mod.rs | 8 ++--- crates/librqbit/src/torrent_state/mod.rs | 12 +++---- 5 files changed, 23 insertions(+), 36 deletions(-) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index e86e31d..41cb617 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context}; +use anyhow::{Context}; use axum::body::Bytes; use axum::extract::{Path, Query, State}; use axum::response::IntoResponse; @@ -472,7 +472,7 @@ impl ApiInternal { }) } - fn api_dump_haves(&self, idx: usize) -> Result { + fn api_dump_haves(&self, _idx: usize) -> Result { Err(anyhow::anyhow!("not implemented").into()) // let mgr = self.mgr_handle(idx)?; // Ok(format!( diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index ddc213d..d4bbd28 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -17,7 +17,7 @@ use crate::{ dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult}, peer_connection::PeerConnectionOptions, spawn_utils::{spawn, BlockingSpawner}, - torrent_state::{ManagedTorrent, ManagedTorrentBuilder, ManagedTorrentHandle}, + torrent_state::{ManagedTorrentBuilder, ManagedTorrentHandle}, }; pub const SUPPORTED_SCHEMES: [&str; 3] = ["http:", "https:", "magnet:"]; diff --git a/crates/librqbit/src/torrent_state/initializing.rs b/crates/librqbit/src/torrent_state/initializing.rs index 8d8bc02..0878be6 100644 --- a/crates/librqbit/src/torrent_state/initializing.rs +++ b/crates/librqbit/src/torrent_state/initializing.rs @@ -1,32 +1,19 @@ use std::{ - collections::HashSet, fs::{File, OpenOptions}, - net::SocketAddr, - path::{Path, PathBuf}, sync::Arc, - time::{Duration, Instant}, + time::Instant, }; use anyhow::Context; -use bencode::from_bytes; -use buffers::ByteString; -use librqbit_core::{ - id20::Id20, lengths::Lengths, peer_id::generate_peer_id, speed_estimator::SpeedEstimator, - torrent_metainfo::TorrentMetaV1Info, -}; + +use librqbit_core::{lengths::Lengths, torrent_metainfo::TorrentMetaV1Info}; use parking_lot::Mutex; -use reqwest::Url; + use sha1w::Sha1; use size_format::SizeFormatterBinary as SF; -use tracing::{debug, info, span, warn, Level}; +use tracing::{debug, info, warn}; -use crate::{ - chunk_tracker::ChunkTracker, - file_ops::FileOps, - spawn_utils::{spawn, BlockingSpawner}, - torrent_state::{ManagedTorrent, ManagedTorrentHandle, TorrentStateLive, TorrentStateOptions}, - tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, -}; +use crate::{chunk_tracker::ChunkTracker, file_ops::FileOps}; use super::{paused::TorrentStatePaused, ManagedTorrentInfo}; @@ -64,7 +51,7 @@ impl TorrentStateInitializing { full_path.push(relative_path); std::fs::create_dir_all(full_path.parent().unwrap())?; - let file = if (&self.meta).options.overwrite { + let file = if self.meta.options.overwrite { OpenOptions::new() .create(true) .read(true) @@ -86,12 +73,12 @@ impl TorrentStateInitializing { }; let lengths = - make_lengths(&(&self.meta).info).context("unable to compute Lengths from torrent")?; + make_lengths(&self.meta.info).context("unable to compute Lengths from torrent")?; debug!("computed lengths: {:?}", &lengths); info!("Doing initial checksum validation, this might take a while..."); - let initial_check_results = (&self.meta).spawner.spawn_block_in_place(|| { - FileOps::::new(&(&self.meta).info, &files, &lengths) + let initial_check_results = self.meta.spawner.spawn_block_in_place(|| { + FileOps::::new(&self.meta.info, &files, &lengths) .initial_check(self.only_files.as_deref()) })?; diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index e59a6d3..b48a206 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -63,8 +63,8 @@ use clone_to_owned::CloneToOwned; use futures::{stream::FuturesUnordered, StreamExt}; use librqbit_core::{ id20::Id20, - lengths::{self, ChunkInfo, Lengths, ValidPieceIndex}, - speed_estimator::{self, SpeedEstimator}, + lengths::{ChunkInfo, Lengths, ValidPieceIndex}, + speed_estimator::{SpeedEstimator}, torrent_metainfo::TorrentMetaV1Info, }; use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -88,7 +88,7 @@ use crate::{ peer_connection::{ PeerConnection, PeerConnectionHandler, PeerConnectionOptions, WriterRequest, }, - spawn_utils::{spawn, BlockingSpawner}, + spawn_utils::{spawn}, tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, type_aliases::{PeerHandle, BF}, }; @@ -165,7 +165,7 @@ impl TorrentStateLive { let have_bytes = paused.chunk_tracker.get_have_bytes(); let needed_bytes = paused.chunk_tracker.get_needed_bytes(); - let lengths = paused.chunk_tracker.get_lengths().clone(); + let lengths = *paused.chunk_tracker.get_lengths(); let state = Arc::new(TorrentStateLive { meta: paused.info.clone(), diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index eda146a..04e050f 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -7,21 +7,21 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use std::{collections::HashSet, path::Path}; +use std::{path::Path}; use anyhow::Context; use buffers::ByteString; use librqbit_core::id20::Id20; use librqbit_core::peer_id::generate_peer_id; -use librqbit_core::speed_estimator::SpeedEstimator; + use librqbit_core::torrent_metainfo::TorrentMetaV1Info; pub use live::*; use parking_lot::RwLock; -use tokio::sync::mpsc::Sender; -use tracing::trace_span; + + use url::Url; -use crate::spawn_utils::{spawn, BlockingSpawner}; +use crate::spawn_utils::{BlockingSpawner}; use initializing::TorrentStateInitializing; @@ -70,7 +70,7 @@ impl ManagedTorrent { self.info.info_hash } - pub(crate) fn add_peer(&self, peer: SocketAddr) -> bool { + pub(crate) fn add_peer(&self, _peer: SocketAddr) -> bool { todo!() } From afbf2a76b96ea1b00fff6f1cf5d72a820f1f7f73 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 14:08:02 +0000 Subject: [PATCH 11/38] Should be working ok now --- TODO.md | 8 +- crates/librqbit/src/chunk_tracker.rs | 8 -- crates/librqbit/src/http_api.rs | 48 +++---- crates/librqbit/src/session.rs | 86 ++++++------ .../src/torrent_state/initializing.rs | 4 +- crates/librqbit/src/torrent_state/live/mod.rs | 13 +- crates/librqbit/src/torrent_state/mod.rs | 126 ++++++++++++++++-- crates/librqbit/src/torrent_state/paused.rs | 9 ++ 8 files changed, 197 insertions(+), 105 deletions(-) diff --git a/TODO.md b/TODO.md index 6090d34..d91f504 100644 --- a/TODO.md +++ b/TODO.md @@ -18,4 +18,10 @@ - [ ] it's sending many requests now way too fast, locks up Mac OS UI annoyingly someday: -- [ ] cancellation from the client-side for the lib (i.e. stop the torrent manager) \ No newline at end of file +- [ ] cancellation from the client-side for the lib (i.e. stop the torrent manager) + + +refactor: +- [ ] where are peers stored +- [ ] http api pause/unpause etc +- [ ] when a live torrent fails writing to disk, it should transition to error state \ No newline at end of file diff --git a/crates/librqbit/src/chunk_tracker.rs b/crates/librqbit/src/chunk_tracker.rs index f5b16e3..21207fc 100644 --- a/crates/librqbit/src/chunk_tracker.rs +++ b/crates/librqbit/src/chunk_tracker.rs @@ -76,14 +76,6 @@ impl ChunkTracker { } } - pub fn get_have_bytes(&self) -> u64 { - todo!() - } - - pub fn get_needed_bytes(&self) -> u64 { - todo!() - } - pub fn get_lengths(&self) -> &Lengths { &self.lengths } diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 41cb617..bd94f2b 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -1,4 +1,4 @@ -use anyhow::{Context}; +use anyhow::Context; use axum::body::Bytes; use axum::extract::{Path, Query, State}; use axum::response::IntoResponse; @@ -20,7 +20,7 @@ use axum::Router; use crate::http_api_error::{ApiError, ApiErrorExt}; use crate::session::{ - AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, + AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, TorrentId, }; use crate::torrent_state::peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot}; use crate::torrent_state::stats::snapshot::StatsSnapshot; @@ -38,9 +38,6 @@ impl HttpApi { inner: Arc::new(ApiInternal::new(session)), } } - pub fn add_torrent_handle(&self, handle: ManagedTorrentHandle) -> usize { - self.inner.add_torrent_handle(handle) - } pub async fn make_http_api_and_run(self, addr: SocketAddr) -> anyhow::Result<()> { let state = self.inner; @@ -331,9 +328,7 @@ impl TorrentAddQueryParams { // Private HTTP API internals. Agnostic of web framework. struct ApiInternal { - dht: Option, startup_time: Instant, - torrent_managers: RwLock>, session: Arc, } @@ -342,41 +337,29 @@ type ApiState = Arc; impl ApiInternal { pub fn new(session: Arc) -> Self { Self { - dht: session.get_dht(), startup_time: Instant::now(), - torrent_managers: RwLock::new(Vec::new()), session, } } - fn add_torrent_handle(&self, handle: ManagedTorrentHandle) -> usize { - let mut g = self.torrent_managers.write(); - let idx = g.len(); - g.push(handle); - idx - } - - fn mgr_handle(&self, idx: usize) -> Result { - self.torrent_managers - .read() + fn mgr_handle(&self, idx: TorrentId) -> Result { + self.session .get(idx) - .cloned() .ok_or(ApiError::torrent_not_found(idx)) } fn api_torrent_list(&self) -> TorrentListResponse { - TorrentListResponse { - torrents: self - .torrent_managers - .read() + let items = self.session.with_torrents(|torrents| { + torrents .iter() .enumerate() .map(|(id, mgr)| TorrentListResponseItem { id, info_hash: mgr.info().info_hash.as_string(), }) - .collect(), - } + .collect() + }); + TorrentListResponse { torrents: items } } fn api_torrent_details(&self, idx: usize) -> Result { @@ -406,10 +389,11 @@ impl ApiInternal { .context("error adding torrent") .with_error_status_code(StatusCode::BAD_REQUEST)? { - AddTorrentResponse::AlreadyManaged(managed) => { + AddTorrentResponse::AlreadyManaged(id, managed) => { return Err(anyhow::anyhow!( - "{:?} is already managed, downloaded to {:?}", + "{:?} is already managed, id={}, downloaded to {:?}", managed.info_hash(), + id, &managed.info().out_dir )) .with_error_status_code(StatusCode::CONFLICT); @@ -423,14 +407,13 @@ impl ApiInternal { details: make_torrent_details(&info_hash, &info, only_files.as_deref()) .context("error making torrent details")?, }, - AddTorrentResponse::Added(handle) => { + AddTorrentResponse::Added(id, handle) => { let details = make_torrent_details( &handle.info_hash(), &handle.info().info, handle.only_files().as_deref(), ) .context("error making torrent details")?; - let id = self.add_torrent_handle(handle); ApiAddTorrentResponse { id: Some(id), details, @@ -441,14 +424,15 @@ impl ApiInternal { } fn api_dht_stats(&self) -> Result { - self.dht + self.session + .get_dht() .as_ref() .map(|d| d.stats()) .ok_or(ApiError::dht_disabled()) } fn api_dht_table(&self) -> Result { - let dht = self.dht.as_ref().ok_or(ApiError::dht_disabled())?; + let dht = self.session.get_dht().ok_or(ApiError::dht_disabled())?; Ok(dht.with_routing_table(|r| r.clone())) } diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index d4bbd28..c5e5703 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -11,39 +11,29 @@ use librqbit_core::{ use parking_lot::RwLock; use reqwest::Url; use tokio_stream::StreamExt; -use tracing::{debug, info, span, warn, Level}; +use tracing::{debug, info, trace_span, warn}; use crate::{ dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult}, peer_connection::PeerConnectionOptions, - spawn_utils::{spawn, BlockingSpawner}, + spawn_utils::BlockingSpawner, torrent_state::{ManagedTorrentBuilder, ManagedTorrentHandle}, }; pub const SUPPORTED_SCHEMES: [&str; 3] = ["http:", "https:", "magnet:"]; +pub type TorrentId = usize; + #[derive(Default)] pub struct SessionLocked { torrents: Vec, } -enum SessionLockedAddTorrentResult { - AlreadyManaged(ManagedTorrentHandle), - Added(usize), -} - impl SessionLocked { - fn add_torrent(&mut self, torrent: ManagedTorrentHandle) -> SessionLockedAddTorrentResult { - if let Some(handle) = self - .torrents - .iter() - .find(|t| t.info_hash() == torrent.info_hash()) - { - return SessionLockedAddTorrentResult::AlreadyManaged(handle.clone()); - } + fn add_torrent(&mut self, torrent: ManagedTorrentHandle) -> TorrentId { let idx = self.torrents.len(); self.torrents.push(torrent); - SessionLockedAddTorrentResult::Added(idx) + idx } } @@ -109,9 +99,9 @@ pub struct ListOnlyResponse { } pub enum AddTorrentResponse { - AlreadyManaged(ManagedTorrentHandle), + AlreadyManaged(TorrentId, ManagedTorrentHandle), ListOnly(ListOnlyResponse), - Added(ManagedTorrentHandle), + Added(TorrentId, ManagedTorrentHandle), } pub fn read_local_file_including_stdin(filename: &str) -> anyhow::Result> { @@ -207,15 +197,14 @@ impl Session { locked: RwLock::new(SessionLocked::default()), }) } - pub fn get_dht(&self) -> Option { - self.dht.clone() + pub fn get_dht(&self) -> Option<&Dht> { + self.dht.as_ref() } - pub fn with_torrents(&self, callback: F) - where - F: Fn(&[ManagedTorrentHandle]), - { + + pub fn with_torrents(&self, callback: impl Fn(&[ManagedTorrentHandle]) -> R) -> R { callback(&self.locked.read().torrents) } + pub async fn add_torrent( &self, add: impl Into>, @@ -411,32 +400,37 @@ impl Session { builder.peer_read_write_timeout(t); } - let managed_torrent = builder.build(); - - match self.locked.write().add_torrent(managed_torrent.clone()) { - SessionLockedAddTorrentResult::AlreadyManaged(managed) => { - return Ok(AddTorrentResponse::AlreadyManaged(managed)) + let (managed_torrent, id) = { + let mut g = self.locked.write(); + if let Some((id, handle)) = g + .torrents + .iter() + .enumerate() + .find(|(_, t)| t.info_hash() == info_hash) + { + return Ok(AddTorrentResponse::AlreadyManaged(id, handle.clone())); } - SessionLockedAddTorrentResult::Added(_) => {} + let managed_torrent = builder.build(); + let id = g.add_torrent(managed_torrent.clone()); + (managed_torrent, id) + }; + + { + let span = trace_span!("torrent", id = id); + let _ = span.enter(); + managed_torrent + .start(initial_peers, dht_peer_rx) + .context("error starting torrent")?; } - for peer in initial_peers { - managed_torrent.add_peer(peer); - } + Ok(AddTorrentResponse::Added(id, managed_torrent)) + } - if let Some(mut dht_peer_rx) = dht_peer_rx { - spawn(span!(Level::INFO, "dht_peer_adder"), { - let handle = managed_torrent.clone(); - async move { - while let Some(peer) = dht_peer_rx.next().await { - handle.add_peer(peer); - } - warn!("dht was closed"); - Ok(()) - } - }); - } + pub fn get(&self, id: TorrentId) -> Option { + self.locked.read().torrents.get(id).cloned() + } - Ok(AddTorrentResponse::Added(managed_torrent)) + pub fn restart(&self, id: usize) -> anyhow::Result<()> { + todo!() } } diff --git a/crates/librqbit/src/torrent_state/initializing.rs b/crates/librqbit/src/torrent_state/initializing.rs index 0878be6..1858878 100644 --- a/crates/librqbit/src/torrent_state/initializing.rs +++ b/crates/librqbit/src/torrent_state/initializing.rs @@ -29,8 +29,8 @@ fn ensure_file_length(file: &File, length: u64) -> anyhow::Result<()> { } pub struct TorrentStateInitializing { - meta: Arc, - only_files: Option>, + pub(crate) meta: Arc, + pub(crate) only_files: Option>, } impl TorrentStateInitializing { diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index b48a206..957ca5d 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -64,7 +64,7 @@ use futures::{stream::FuturesUnordered, StreamExt}; use librqbit_core::{ id20::Id20, lengths::{ChunkInfo, Lengths, ValidPieceIndex}, - speed_estimator::{SpeedEstimator}, + speed_estimator::SpeedEstimator, torrent_metainfo::TorrentMetaV1Info, }; use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -88,7 +88,7 @@ use crate::{ peer_connection::{ PeerConnection, PeerConnectionHandler, PeerConnectionOptions, WriterRequest, }, - spawn_utils::{spawn}, + spawn_utils::spawn, tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, type_aliases::{PeerHandle, BF}, }; @@ -157,14 +157,13 @@ pub struct TorrentStateLive { } impl TorrentStateLive { - #[allow(clippy::too_many_arguments)] pub(crate) fn new(paused: TorrentStatePaused) -> Arc { let (peer_queue_tx, peer_queue_rx) = unbounded_channel(); let speed_estimator = SpeedEstimator::new(5); - let have_bytes = paused.chunk_tracker.get_have_bytes(); - let needed_bytes = paused.chunk_tracker.get_needed_bytes(); + let have_bytes = paused.have_bytes; + let needed_bytes = paused.needed_bytes; let lengths = *paused.chunk_tracker.get_lengths(); let state = Arc::new(TorrentStateLive { @@ -560,6 +559,10 @@ impl TorrentStateLive { } self.finished_notify.notified().await; } + + pub fn pause(&self) -> anyhow::Result { + bail!("pause not implemented yet") + } } struct PeerHandlerLocked { diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 04e050f..9de0d9a 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -4,11 +4,12 @@ pub mod paused; pub mod utils; use std::net::SocketAddr; +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use std::{path::Path}; +use anyhow::bail; use anyhow::Context; use buffers::ByteString; use librqbit_core::id20::Id20; @@ -18,20 +19,38 @@ use librqbit_core::torrent_metainfo::TorrentMetaV1Info; pub use live::*; use parking_lot::RwLock; - +use tokio_stream::StreamExt; +use tracing::trace_span; use url::Url; -use crate::spawn_utils::{BlockingSpawner}; +use crate::spawn_utils::spawn; +use crate::spawn_utils::BlockingSpawner; use initializing::TorrentStateInitializing; use self::paused::TorrentStatePaused; pub enum ManagedTorrentState { - Initializing(TorrentStateInitializing), + Initializing(Arc), Paused(TorrentStatePaused), Live(Arc), Error(anyhow::Error), + + // This is used when swapping between states, outside world should never see it. + None, +} + +impl ManagedTorrentState { + fn assert_paused(self) -> TorrentStatePaused { + match self { + Self::Paused(paused) => paused, + _ => panic!("Expected paused state"), + } + } + + fn take(&mut self) -> Self { + std::mem::replace(self, Self::None) + } } pub(crate) struct ManagedTorrentLocked { @@ -58,6 +77,7 @@ pub struct ManagedTorrentInfo { pub struct ManagedTorrent { pub info: Arc, + only_files: Option>, locked: RwLock, } @@ -70,13 +90,8 @@ impl ManagedTorrent { self.info.info_hash } - pub(crate) fn add_peer(&self, _peer: SocketAddr) -> bool { - todo!() - } - pub fn only_files(&self) -> Option> { - // self.locked.write().only_files.clone() - todo!() + self.only_files.clone() } pub fn with_state(&self, f: impl FnOnce(&ManagedTorrentState) -> R) -> R { @@ -91,6 +106,91 @@ impl ManagedTorrent { } } + pub fn start( + self: &Arc, + initial_peers: Vec, + peer_rx: Option + Unpin + Send + Sync + 'static>, + ) -> anyhow::Result<()> { + let mut g = self.locked.write(); + match &g.state { + ManagedTorrentState::Live(_) => { + bail!("torrent is already live"); + } + ManagedTorrentState::Initializing(init) => { + let init = init.clone(); + let t = self.clone(); + spawn(trace_span!("initialize_and_start"), async move { + match init.check().await { + Ok(paused) => { + let live = TorrentStateLive::new(paused); + t.locked.write().state = ManagedTorrentState::Live(live.clone()); + + let live = Arc::downgrade(&live); + spawn(trace_span!("peer_adder"), async move { + { + let live: Arc = + live.upgrade().context("no longer live")?; + for peer in initial_peers { + live.add_peer_if_not_seen(peer); + } + } + + if let Some(mut peer_rx) = peer_rx { + while let Some(peer) = peer_rx.next().await { + live.upgrade() + .context("no longer live")? + .add_peer_if_not_seen(peer); + } + } + + Ok(()) + }); + + Ok(()) + } + Err(err) => { + let result = anyhow::anyhow!("{:?}", err); + t.locked.write().state = ManagedTorrentState::Error(err); + Err(result) + } + } + }); + Ok(()) + } + ManagedTorrentState::Paused(_) => { + let paused = g.state.take().assert_paused(); + let live = TorrentStateLive::new(paused); + g.state = ManagedTorrentState::Live(live); + Ok(()) + } + ManagedTorrentState::Error(_) => { + bail!("starting torrents from error state not implemented") + } + ManagedTorrentState::None => bail!("bug: torrent is in empty state"), + } + } + + pub fn pause(&self) -> anyhow::Result<()> { + let mut g = self.locked.write(); + match &g.state { + ManagedTorrentState::Live(live) => { + let paused = live.pause()?; + g.state = ManagedTorrentState::Paused(paused); + Ok(()) + } + ManagedTorrentState::Initializing(_) => { + bail!("torrent is initializing, can't pause"); + } + ManagedTorrentState::Paused(_) => { + bail!("torrent is already paused"); + } + ManagedTorrentState::Error(_) => { + bail!("can't pause torrent in error state") + } + ManagedTorrentState::None => bail!("bug: torrent is in empty state"), + } + } + pub async fn wait_until_completed(&self) -> anyhow::Result<()> { // TODO: rewrite self.live() @@ -191,8 +291,12 @@ impl ManagedTorrentBuilder { overwrite: self.overwrite, }, }); - let initializing = TorrentStateInitializing::new(info.clone(), self.only_files); + let initializing = Arc::new(TorrentStateInitializing::new( + info.clone(), + self.only_files.clone(), + )); Arc::new(ManagedTorrent { + only_files: self.only_files, locked: RwLock::new(ManagedTorrentLocked { state: ManagedTorrentState::Initializing(initializing), }), diff --git a/crates/librqbit/src/torrent_state/paused.rs b/crates/librqbit/src/torrent_state/paused.rs index 0a2b2c9..b8ac7c1 100644 --- a/crates/librqbit/src/torrent_state/paused.rs +++ b/crates/librqbit/src/torrent_state/paused.rs @@ -14,3 +14,12 @@ pub struct TorrentStatePaused { pub(crate) have_bytes: u64, pub(crate) needed_bytes: u64, } + +// impl TorrentStatePaused { +// pub fn get_have_bytes(&self) -> u64 { +// self.have_bytes +// } +// pub fn get_needed_bytes(&self) -> u64 { +// self.needed_bytes +// } +// } From 0c4844f5345d3f1f94c809469f0cd01f58330e40 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 14:19:39 +0000 Subject: [PATCH 12/38] 1/n Add pause/start actions --- crates/dht/src/dht.rs | 3 ++- crates/librqbit/src/dht_utils.rs | 2 +- crates/librqbit/src/http_api.rs | 42 ++++++++++++++++++++++++++++---- crates/librqbit/src/session.rs | 15 ++++++++---- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index 3808ecd..bcbc576 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -690,7 +690,8 @@ impl Dht { }); Ok(Dht { state }) } - pub async fn get_peers( + + pub fn get_peers( &self, info_hash: Id20, ) -> anyhow::Result + Unpin> { diff --git a/crates/librqbit/src/dht_utils.rs b/crates/librqbit/src/dht_utils.rs index 2c9f3fc..9e7d60f 100644 --- a/crates/librqbit/src/dht_utils.rs +++ b/crates/librqbit/src/dht_utils.rs @@ -107,7 +107,7 @@ mod tests { let info_hash = Id20::from_str("cf3ea75e2ebbd30e0da6e6e215e2226bf35f2e33").unwrap(); let dht = Dht::new().await.unwrap(); - let peer_rx = dht.get_peers(info_hash).await.unwrap(); + let peer_rx = dht.get_peers(info_hash).unwrap(); let peer_id = generate_peer_id(); match read_metainfo_from_peer_receiver(peer_id, info_hash, peer_rx, None).await { ReadMetainfoResult::Found { info, .. } => dbg!(info), diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index bd94f2b..0f56493 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -2,7 +2,7 @@ use anyhow::Context; use axum::body::Bytes; use axum::extract::{Path, Query, State}; use axum::response::IntoResponse; -use axum::routing::get; +use axum::routing::{get, post}; use buffers::ByteString; use dht::{Dht, DhtStats}; use http::StatusCode; @@ -116,6 +116,20 @@ impl HttpApi { state.api_peer_stats(idx, filter).map(axum::Json) } + async fn torrent_action_pause( + State(state): State, + Path(idx): Path, + ) -> Result { + state.api_torrent_action_pause(idx) + } + + async fn torrent_action_start( + State(state): State, + Path(idx): Path, + ) -> Result { + state.api_torrent_action_start(idx) + } + #[allow(unused_mut)] let mut app = Router::new() .route("/", get(api_root)) @@ -125,7 +139,9 @@ impl HttpApi { .route("/torrents/:id", get(torrent_details)) .route("/torrents/:id/haves", get(torrent_haves)) .route("/torrents/:id/stats", get(torrent_stats)) - .route("/torrents/:id/peer_stats", get(peer_stats)); + .route("/torrents/:id/peer_stats", get(peer_stats)) + .route("/torrents/:id/pause", post(torrent_action_pause)) + .route("/torrents/:id/start", post(torrent_action_start)); #[cfg(feature = "webui")] { @@ -362,14 +378,14 @@ impl ApiInternal { TorrentListResponse { torrents: items } } - fn api_torrent_details(&self, idx: usize) -> Result { + fn api_torrent_details(&self, idx: TorrentId) -> Result { let handle = self.mgr_handle(idx)?; let info_hash = handle.info().info_hash; let only_files = handle.only_files(); make_torrent_details(&info_hash, &handle.info().info, only_files.as_deref()) } - fn api_peer_stats(&self, idx: usize, filter: PeerStatsFilter) -> Result { + fn api_peer_stats(&self, idx: TorrentId, filter: PeerStatsFilter) -> Result { let handle = self.mgr_handle(idx)?; Ok(handle .live() @@ -377,6 +393,22 @@ impl ApiInternal { .per_peer_stats_snapshot(filter)) } + fn api_torrent_action_pause(&self, idx: TorrentId) -> Result<()> { + let handle = self.mgr_handle(idx)?; + handle + .pause() + .context("error pausing torrent") + .with_error_status_code(StatusCode::BAD_REQUEST) + } + + fn api_torrent_action_start(&self, idx: TorrentId) -> Result<()> { + let handle = self.mgr_handle(idx)?; + self.session + .unpause(&handle) + .context("error unpausing torrent") + .with_error_status_code(StatusCode::BAD_REQUEST) + } + pub async fn api_add_torrent( &self, add: AddTorrent<'_>, @@ -436,7 +468,7 @@ impl ApiInternal { Ok(dht.with_routing_table(|r| r.clone())) } - fn api_stats(&self, idx: usize) -> Result { + fn api_stats(&self, idx: TorrentId) -> Result { let mgr = self.mgr_handle(idx)?; let live = mgr.live().context("not live")?; let snapshot = live.stats_snapshot(); diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index c5e5703..fe7c0ec 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -224,8 +224,7 @@ impl Session { .dht .as_ref() .context("magnet links without DHT are not supported")? - .get_peers(info_hash) - .await?; + .get_peers(info_hash)?; let trackers = trackers .into_iter() @@ -274,7 +273,7 @@ impl Session { let dht_rx = match self.dht.as_ref() { Some(dht) => { debug!("reading peers for {:?} from DHT", torrent.info_hash); - Some(dht.get_peers(torrent.info_hash).await?) + Some(dht.get_peers(torrent.info_hash)?) } None => None, }; @@ -430,7 +429,13 @@ impl Session { self.locked.read().torrents.get(id).cloned() } - pub fn restart(&self, id: usize) -> anyhow::Result<()> { - todo!() + pub fn unpause(&self, handle: &ManagedTorrentHandle) -> anyhow::Result<()> { + let peer_rx = self + .dht + .as_ref() + .map(|dht| dht.get_peers(handle.info_hash())) + .transpose()?; + handle.start(Default::default(), peer_rx); + return Ok(()); } } From b79a21179f9681b16b7286d2bbf5ba0f0c29bf90 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 14:29:05 +0000 Subject: [PATCH 13/38] 2/n Add pause/start actions --- crates/librqbit/src/http_api.rs | 18 ++++-------------- crates/librqbit/src/session.rs | 7 +++++-- crates/librqbit/src/torrent_state/mod.rs | 10 ++++++++++ 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 0f56493..179e2c6 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -4,12 +4,11 @@ use axum::extract::{Path, Query, State}; use axum::response::IntoResponse; use axum::routing::{get, post}; use buffers::ByteString; -use dht::{Dht, DhtStats}; +use dht::DhtStats; use http::StatusCode; use itertools::Itertools; use librqbit_core::id20::Id20; use librqbit_core::torrent_metainfo::TorrentMetaV1Info; -use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::sync::Arc; @@ -367,8 +366,6 @@ impl ApiInternal { fn api_torrent_list(&self) -> TorrentListResponse { let items = self.session.with_torrents(|torrents| { torrents - .iter() - .enumerate() .map(|(id, mgr)| TorrentListResponseItem { id, info_hash: mgr.info().info_hash.as_string(), @@ -488,16 +485,9 @@ impl ApiInternal { }) } - fn api_dump_haves(&self, _idx: usize) -> Result { - Err(anyhow::anyhow!("not implemented").into()) - // let mgr = self.mgr_handle(idx)?; - // Ok(format!( - // "{:?}", - // mgr.live().conetext() - // .lock_read("api_dump_haves") - // .chunks - // .get_have_pieces(), - // )) + fn api_dump_haves(&self, idx: usize) -> Result { + let mgr = self.mgr_handle(idx)?; + Ok(mgr.with_chunk_tracker(|chunks| format!("{:?}", chunks.get_have_pieces()))?) } } diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index fe7c0ec..2809973 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -201,8 +201,11 @@ impl Session { self.dht.as_ref() } - pub fn with_torrents(&self, callback: impl Fn(&[ManagedTorrentHandle]) -> R) -> R { - callback(&self.locked.read().torrents) + pub fn with_torrents( + &self, + callback: impl Fn(&mut dyn Iterator) -> R, + ) -> R { + callback(&mut self.locked.read().torrents.iter().enumerate()) } pub async fn add_torrent( diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 9de0d9a..1451da4 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -23,6 +23,7 @@ use tokio_stream::StreamExt; use tracing::trace_span; use url::Url; +use crate::chunk_tracker::ChunkTracker; use crate::spawn_utils::spawn; use crate::spawn_utils::BlockingSpawner; @@ -98,6 +99,15 @@ impl ManagedTorrent { f(&self.locked.read().state) } + pub fn with_chunk_tracker(&self, f: impl FnOnce(&ChunkTracker) -> R) -> anyhow::Result { + let g = self.locked.read(); + match &g.state { + ManagedTorrentState::Paused(p) => Ok(f(&p.chunk_tracker)), + ManagedTorrentState::Live(l) => Ok(f(&l.lock_read("chunk_tracker").chunks)), + _ => bail!("no chunk tracker, torrent neither paused nor live"), + } + } + pub fn live(&self) -> Option> { let g = self.locked.read(); match &g.state { From 876afbf41bdc18a910663d2d3d758dfb72492647 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 15:04:36 +0000 Subject: [PATCH 14/38] Initialization progress reporting --- crates/librqbit/src/file_ops.rs | 7 +- crates/librqbit/src/http_api.rs | 83 ++++++++++++++++--- crates/librqbit/src/session.rs | 2 +- .../src/torrent_state/initializing.rs | 28 +++---- crates/librqbit/src/torrent_state/live/mod.rs | 3 +- .../src/torrent_state/live/stats/snapshot.rs | 6 +- crates/librqbit/src/torrent_state/mod.rs | 10 ++- crates/librqbit/src/torrent_state/paused.rs | 1 - crates/librqbit_core/src/lengths.rs | 9 +- 9 files changed, 109 insertions(+), 40 deletions(-) diff --git a/crates/librqbit/src/file_ops.rs b/crates/librqbit/src/file_ops.rs index e413dc4..aee6625 100644 --- a/crates/librqbit/src/file_ops.rs +++ b/crates/librqbit/src/file_ops.rs @@ -2,7 +2,10 @@ use std::{ fs::File, io::{Read, Seek, SeekFrom, Write}, marker::PhantomData, - sync::Arc, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, }; use anyhow::Context; @@ -67,6 +70,7 @@ impl<'a, Sha1Impl: ISha1> FileOps<'a, Sha1Impl> { pub fn initial_check( &self, only_files: Option<&[usize]>, + progress: &AtomicU64, ) -> anyhow::Result { let mut needed_pieces = BF::from_vec(vec![0u8; self.lengths.piece_bitfield_bytes()]); let mut have_pieces = BF::from_vec(vec![0u8; self.lengths.piece_bitfield_bytes()]); @@ -125,6 +129,7 @@ impl<'a, Sha1Impl: ISha1> FileOps<'a, Sha1Impl> { let mut piece_remaining = piece_info.len as usize; let mut some_files_broken = false; let mut at_least_one_file_required = current_file.full_file_required; + progress.fetch_add(piece_info.len as u64, Ordering::Relaxed); while piece_remaining > 0 { let mut to_read_in_file = diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 179e2c6..87bb46c 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -11,6 +11,7 @@ use librqbit_core::id20::Id20; use librqbit_core::torrent_metainfo::TorrentMetaV1Info; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::{Duration, Instant}; use tracing::{info, warn}; @@ -23,7 +24,7 @@ use crate::session::{ }; use crate::torrent_state::peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot}; use crate::torrent_state::stats::snapshot::StatsSnapshot; -use crate::torrent_state::ManagedTorrentHandle; +use crate::torrent_state::{ManagedTorrentHandle, ManagedTorrentState, TorrentStateLive}; // Public API #[derive(Clone)] @@ -100,11 +101,18 @@ impl HttpApi { state.api_dump_haves(idx) } - async fn torrent_stats( + async fn torrent_stats_v0( State(state): State, Path(idx): Path, ) -> Result { - state.api_stats(idx).map(axum::Json) + state.api_stats_v0(idx).map(axum::Json) + } + + async fn torrent_stats_v1( + State(state): State, + Path(idx): Path, + ) -> Result { + state.api_stats_v1(idx).map(axum::Json) } async fn peer_stats( @@ -137,7 +145,8 @@ impl HttpApi { .route("/torrents", get(torrents_list).post(torrents_post)) .route("/torrents/:id", get(torrent_details)) .route("/torrents/:id/haves", get(torrent_haves)) - .route("/torrents/:id/stats", get(torrent_stats)) + .route("/torrents/:id/stats", get(torrent_stats_v0)) + .route("/torrents/:id/stats/v1", get(torrent_stats_v1)) .route("/torrents/:id/peer_stats", get(peer_stats)) .route("/torrents/:id/pause", post(torrent_action_pause)) .route("/torrents/:id/start", post(torrent_action_start)); @@ -196,7 +205,7 @@ impl HttpApi { type Result = std::result::Result; -#[derive(Serialize)] +#[derive(Serialize, Default)] struct Speed { mbps: f64, human_readable: String, @@ -261,8 +270,8 @@ impl Serialize for DurationWithHumanReadable { } } -#[derive(Serialize)] -struct StatsResponse { +#[derive(Serialize, Default)] +struct LiveStats { snapshot: StatsSnapshot, average_piece_download_time: Option, download_speed: Speed, @@ -270,6 +279,15 @@ struct StatsResponse { time_remaining: Option, } +#[derive(Serialize)] +struct StatsResponse { + state: &'static str, + error: Option, + progress_bytes: u64, + total_bytes: u64, + live: Option, +} + #[derive(Serialize, Deserialize)] pub struct ApiAddTorrentResponse { pub id: Option, @@ -465,9 +483,7 @@ impl ApiInternal { Ok(dht.with_routing_table(|r| r.clone())) } - fn api_stats(&self, idx: TorrentId) -> Result { - let mgr = self.mgr_handle(idx)?; - let live = mgr.live().context("not live")?; + fn make_live_stats(&self, live: &TorrentStateLive) -> LiveStats { let snapshot = live.stats_snapshot(); let estimator = live.speed_estimator(); @@ -476,12 +492,57 @@ impl ApiInternal { let downloaded_bytes = snapshot.downloaded_and_checked_bytes; let downloaded_mb = downloaded_bytes as f64 / 1024f64 / 1024f64; - Ok(StatsResponse { + LiveStats { average_piece_download_time: snapshot.average_piece_download_time(), snapshot, all_time_download_speed: (downloaded_mb / elapsed.as_secs_f64()).into(), download_speed: estimator.download_mbps().into(), time_remaining: estimator.time_remaining().map(DurationWithHumanReadable), + } + } + + fn api_stats_v0(&self, idx: TorrentId) -> Result { + let mgr = self.mgr_handle(idx)?; + let live = mgr.live().context("torrent not live")?; + Ok(self.make_live_stats(&live)) + } + + fn api_stats_v1(&self, idx: TorrentId) -> Result { + let mgr = self.mgr_handle(idx)?; + let mut resp = StatsResponse { + total_bytes: mgr.info().lengths.total_length(), + state: "", + error: None, + progress_bytes: 0, + live: None, + }; + + mgr.with_state(|s| { + match s { + ManagedTorrentState::Initializing(i) => { + resp.state = "initializing"; + resp.progress_bytes = i.checked_bytes.load(Ordering::Relaxed); + } + ManagedTorrentState::Paused(p) => { + resp.state = "paused"; + resp.progress_bytes = p.have_bytes; + } + ManagedTorrentState::Live(l) => { + resp.state = "live"; + let live_stats = self.make_live_stats(l); + resp.progress_bytes = live_stats.snapshot.downloaded_and_checked_bytes; + resp.live = Some(live_stats); + } + ManagedTorrentState::Error(e) => { + resp.state = "error"; + resp.error = Some(format!("{:?}", e)) + } + ManagedTorrentState::None => { + resp.state = "error"; + resp.error = Some("bug: torrent in broken \"None\" state".to_string()); + } + } + Ok(resp) }) } diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 2809973..f3af02c 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -412,7 +412,7 @@ impl Session { { return Ok(AddTorrentResponse::AlreadyManaged(id, handle.clone())); } - let managed_torrent = builder.build(); + let managed_torrent = builder.build()?; let id = g.add_torrent(managed_torrent.clone()); (managed_torrent, id) }; diff --git a/crates/librqbit/src/torrent_state/initializing.rs b/crates/librqbit/src/torrent_state/initializing.rs index 1858878..c40d2db 100644 --- a/crates/librqbit/src/torrent_state/initializing.rs +++ b/crates/librqbit/src/torrent_state/initializing.rs @@ -1,12 +1,11 @@ use std::{ fs::{File, OpenOptions}, - sync::Arc, + sync::{atomic::AtomicU64, Arc}, time::Instant, }; use anyhow::Context; -use librqbit_core::{lengths::Lengths, torrent_metainfo::TorrentMetaV1Info}; use parking_lot::Mutex; use sha1w::Sha1; @@ -17,13 +16,6 @@ use crate::{chunk_tracker::ChunkTracker, file_ops::FileOps}; use super::{paused::TorrentStatePaused, ManagedTorrentInfo}; -fn make_lengths>( - torrent: &TorrentMetaV1Info, -) -> anyhow::Result { - let total_length = torrent.iter_file_lengths()?.sum(); - Lengths::new(total_length, torrent.piece_length, None) -} - fn ensure_file_length(file: &File, length: u64) -> anyhow::Result<()> { Ok(file.set_len(length)?) } @@ -31,11 +23,16 @@ fn ensure_file_length(file: &File, length: u64) -> anyhow::Result<()> { pub struct TorrentStateInitializing { pub(crate) meta: Arc, pub(crate) only_files: Option>, + pub(crate) checked_bytes: AtomicU64, } impl TorrentStateInitializing { pub fn new(meta: Arc, only_files: Option>) -> Self { - Self { meta, only_files } + Self { + meta, + only_files, + checked_bytes: AtomicU64::new(0), + } } pub async fn check(&self) -> anyhow::Result { @@ -72,14 +69,12 @@ impl TorrentStateInitializing { (files, filenames) }; - let lengths = - make_lengths(&self.meta.info).context("unable to compute Lengths from torrent")?; - debug!("computed lengths: {:?}", &lengths); + debug!("computed lengths: {:?}", &self.meta.lengths); info!("Doing initial checksum validation, this might take a while..."); let initial_check_results = self.meta.spawner.spawn_block_in_place(|| { - FileOps::::new(&self.meta.info, &files, &lengths) - .initial_check(self.only_files.as_deref()) + FileOps::::new(&self.meta.info, &files, &self.meta.lengths) + .initial_check(self.only_files.as_deref(), &self.checked_bytes) })?; info!( @@ -122,7 +117,7 @@ impl TorrentStateInitializing { let chunk_tracker = ChunkTracker::new( initial_check_results.needed_pieces, initial_check_results.have_pieces, - lengths, + self.meta.lengths, ); let paused = TorrentStatePaused { @@ -131,7 +126,6 @@ impl TorrentStateInitializing { filenames, chunk_tracker, have_bytes: initial_check_results.have_bytes, - needed_bytes: initial_check_results.needed_bytes, }; Ok(paused) } diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 957ca5d..68a96c8 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -163,7 +163,7 @@ impl TorrentStateLive { let speed_estimator = SpeedEstimator::new(5); let have_bytes = paused.have_bytes; - let needed_bytes = paused.needed_bytes; + let needed_bytes = paused.info.lengths.total_length() - have_bytes; let lengths = *paused.chunk_tracker.get_lengths(); let state = Arc::new(TorrentStateLive { @@ -533,7 +533,6 @@ impl TorrentStateLive { fetched_bytes: self.stats.fetched_bytes.load(Relaxed), uploaded_bytes: self.stats.uploaded_bytes.load(Relaxed), total_bytes: self.have_plus_needed_bytes, - time: Instant::now(), initially_needed_bytes: self.needed_bytes, remaining_bytes: remaining, total_piece_download_ms: self.stats.total_piece_download_ms.load(Relaxed), diff --git a/crates/librqbit/src/torrent_state/live/stats/snapshot.rs b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs index f22b8be..45dce43 100644 --- a/crates/librqbit/src/torrent_state/live/stats/snapshot.rs +++ b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs @@ -4,7 +4,7 @@ use serde::Serialize; use crate::torrent_state::live::peers::stats::snapshot::AggregatePeerStats; -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Default)] pub struct StatsSnapshot { pub have_bytes: u64, pub downloaded_and_checked_bytes: u64, @@ -14,8 +14,8 @@ pub struct StatsSnapshot { pub initially_needed_bytes: u64, pub remaining_bytes: u64, pub total_bytes: u64, - #[serde(skip)] - pub time: Instant, + // #[serde(skip)] + // pub time: Instant, pub total_piece_download_ms: u64, pub peer_stats: AggregatePeerStats, } diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 1451da4..a81a59f 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -13,6 +13,7 @@ use anyhow::bail; use anyhow::Context; use buffers::ByteString; use librqbit_core::id20::Id20; +use librqbit_core::lengths::Lengths; use librqbit_core::peer_id::generate_peer_id; use librqbit_core::torrent_metainfo::TorrentMetaV1Info; @@ -73,6 +74,7 @@ pub struct ManagedTorrentInfo { pub spawner: BlockingSpawner, pub trackers: Vec, pub peer_id: Id20, + pub lengths: Lengths, pub(crate) options: ManagedTorrentOptions, } @@ -286,7 +288,8 @@ impl ManagedTorrentBuilder { self } - pub(crate) fn build(self) -> ManagedTorrentHandle { + pub(crate) fn build(self) -> anyhow::Result { + let lengths = Lengths::from_torrent(&self.info)?; let info = Arc::new(ManagedTorrentInfo { info: self.info, info_hash: self.info_hash, @@ -294,6 +297,7 @@ impl ManagedTorrentBuilder { trackers: self.trackers.into_iter().collect(), spawner: self.spawner.unwrap_or_default(), peer_id: self.peer_id.unwrap_or_else(generate_peer_id), + lengths, options: ManagedTorrentOptions { force_tracker_interval: self.force_tracker_interval, peer_connect_timeout: self.peer_connect_timeout, @@ -305,13 +309,13 @@ impl ManagedTorrentBuilder { info.clone(), self.only_files.clone(), )); - Arc::new(ManagedTorrent { + Ok(Arc::new(ManagedTorrent { only_files: self.only_files, locked: RwLock::new(ManagedTorrentLocked { state: ManagedTorrentState::Initializing(initializing), }), info, - }) + })) } } diff --git a/crates/librqbit/src/torrent_state/paused.rs b/crates/librqbit/src/torrent_state/paused.rs index b8ac7c1..62a4553 100644 --- a/crates/librqbit/src/torrent_state/paused.rs +++ b/crates/librqbit/src/torrent_state/paused.rs @@ -12,7 +12,6 @@ pub struct TorrentStatePaused { pub(crate) filenames: Vec, pub(crate) chunk_tracker: ChunkTracker, pub(crate) have_bytes: u64, - pub(crate) needed_bytes: u64, } // impl TorrentStatePaused { diff --git a/crates/librqbit_core/src/lengths.rs b/crates/librqbit_core/src/lengths.rs index 05c7c82..64855ad 100644 --- a/crates/librqbit_core/src/lengths.rs +++ b/crates/librqbit_core/src/lengths.rs @@ -1,4 +1,4 @@ -use crate::constants::CHUNK_SIZE; +use crate::{constants::CHUNK_SIZE, torrent_metainfo::TorrentMetaV1Info}; const fn is_power_of_two(x: u64) -> bool { (x != 0) && ((x & (x - 1)) == 0) @@ -61,6 +61,13 @@ impl ValidPieceIndex { } impl Lengths { + pub fn from_torrent>( + torrent: &TorrentMetaV1Info, + ) -> anyhow::Result { + let total_length = torrent.iter_file_lengths()?.sum(); + Lengths::new(total_length, torrent.piece_length, None) + } + pub fn new( total_length: u64, piece_length: u32, From 66d2f224edf5dd4529486f0b728fd7dcc9acd1b5 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 15:36:37 +0000 Subject: [PATCH 15/38] Switch UI to display statuses better --- crates/librqbit/src/chunk_tracker.rs | 4 +- crates/librqbit/src/http_api.rs | 6 ++- crates/librqbit/src/session.rs | 4 +- .../src/torrent_state/live/stats/snapshot.rs | 2 +- crates/librqbit/webui/src/api.ts | 29 ++++++++++- crates/librqbit/webui/src/index.tsx | 49 ++++++++++++++----- crates/rqbit/src/main.rs | 12 ++--- 7 files changed, 80 insertions(+), 26 deletions(-) diff --git a/crates/librqbit/src/chunk_tracker.rs b/crates/librqbit/src/chunk_tracker.rs index 21207fc..ecaeab5 100644 --- a/crates/librqbit/src/chunk_tracker.rs +++ b/crates/librqbit/src/chunk_tracker.rs @@ -4,7 +4,7 @@ use tracing::{debug, info}; use crate::type_aliases::BF; -pub(crate) struct ChunkTracker { +pub struct ChunkTracker { // This forms the basis of a "queue" to pull from. // It's set to 1 if we need a piece, but the moment we start requesting a peer, // it's set to 0. @@ -51,7 +51,7 @@ fn compute_chunk_status(lengths: &Lengths, needed_pieces: &BF) -> BF { chunk_bf } -pub(crate) enum ChunkMarkingResult { +pub enum ChunkMarkingResult { PreviouslyCompleted, NotCompleted, Completed, diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 87bb46c..2d4edc1 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -285,6 +285,7 @@ struct StatsResponse { error: Option, progress_bytes: u64, total_bytes: u64, + finished: bool, live: Option, } @@ -514,6 +515,7 @@ impl ApiInternal { state: "", error: None, progress_bytes: 0, + finished: false, live: None, }; @@ -526,11 +528,13 @@ impl ApiInternal { ManagedTorrentState::Paused(p) => { resp.state = "paused"; resp.progress_bytes = p.have_bytes; + resp.finished = p.have_bytes == resp.progress_bytes; } ManagedTorrentState::Live(l) => { resp.state = "live"; let live_stats = self.make_live_stats(l); - resp.progress_bytes = live_stats.snapshot.downloaded_and_checked_bytes; + resp.progress_bytes = live_stats.snapshot.have_bytes; + resp.finished = resp.progress_bytes == resp.total_bytes; resp.live = Some(live_stats); } ManagedTorrentState::Error(e) => { diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index f3af02c..2d1d656 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -438,7 +438,7 @@ impl Session { .as_ref() .map(|dht| dht.get_peers(handle.info_hash())) .transpose()?; - handle.start(Default::default(), peer_rx); - return Ok(()); + handle.start(Default::default(), peer_rx)?; + Ok(()) } } diff --git a/crates/librqbit/src/torrent_state/live/stats/snapshot.rs b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs index 45dce43..6331b6a 100644 --- a/crates/librqbit/src/torrent_state/live/stats/snapshot.rs +++ b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs @@ -1,4 +1,4 @@ -use std::time::{Duration, Instant}; +use std::time::Duration; use serde::Serialize; diff --git a/crates/librqbit/webui/src/api.ts b/crates/librqbit/webui/src/api.ts index 915bc10..e99026f 100644 --- a/crates/librqbit/webui/src/api.ts +++ b/crates/librqbit/webui/src/api.ts @@ -29,7 +29,7 @@ export interface ListTorrentsResponse { } // Interface for the Torrent Stats API response -export interface TorrentStats { +export interface LiveTorrentStats { snapshot: { have_bytes: number; downloaded_and_checked_bytes: number; @@ -69,6 +69,15 @@ export interface TorrentStats { } | null; } +export interface TorrentStats { + state: string, + error: string | null, + progress_bytes: number, + finished: boolean, + total_bytes: number, + live: LiveTorrentStats | null; +} + export interface ErrorDetails { id?: number, @@ -129,7 +138,7 @@ export const API = { return makeRequest('GET', `/torrents/${index}`); }, getTorrentStats: (index: number): Promise => { - return makeRequest('GET', `/torrents/${index}/stats`); + return makeRequest('GET', `/torrents/${index}/stats/v1`); }, uploadTorrent: (data: string | File, opts?: { @@ -144,5 +153,21 @@ export const API = { url += `&only_files=${opts.selectedFiles.join(',')}`; } return makeRequest('POST', url, data) + }, + + pause: (index: number): Promise => { + return makeRequest('POST', `/torrents/${index}/pause`); + }, + + start: (index: number): Promise => { + return makeRequest('POST', `/torrents/${index}/start`); + }, + + forget: (index: number): Promise => { + return makeRequest('POST', `/torrents/${index}/forget`); + }, + + delete: (index: number): Promise => { + return makeRequest('POST', `/torrents/${index}/delete`); } } \ No newline at end of file diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 7baee31..40b219f 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -18,13 +18,33 @@ const AppContext = createContext(null); const TorrentRow: React.FC<{ id: number, detailsResponse: TorrentDetails, statsResponse: TorrentStats }> = ({ id, detailsResponse, statsResponse }) => { - const totalBytes = statsResponse?.snapshot?.total_bytes ?? 1; - const downloadedBytes = statsResponse?.snapshot?.have_bytes ?? 0; - const finished = totalBytes == downloadedBytes; - const downloadPercentage = (downloadedBytes / totalBytes) * 100; + const state = statsResponse?.state ?? ""; + + const totalBytes = statsResponse?.total_bytes ?? 1; + const progressBytes = statsResponse?.progress_bytes ?? 0; + const finished = statsResponse?.finished || false; + const progressPercentage = state == 'error' ? 100 : (progressBytes / totalBytes) * 100; + const isAnimated = (state == "initializing" || state == "live") && !finished; + const progressLabel = state == 'error' ? 'Error' : `${progressPercentage.toFixed(2)}%`; + + const getPeersString = (statsResponse: TorrentStats) => { + let peer_stats = statsResponse?.live?.snapshot.peer_stats; + if (!peer_stats) { + return ''; + } + return `${peer_stats.live} / ${peer_stats.seen}`; + } + + let classNames = []; + if (id % 2 == 0) { + classNames.push('bg-light'); + } + if (statsResponse?.error) { + classNames.push('bg-warning'); + } return ( - + {detailsResponse ?
@@ -36,11 +56,16 @@ const TorrentRow: React.FC<{ <> {`${formatBytes(totalBytes)} `} - + + { + statsResponse.error && ( +

{statsResponse.error}

+ ) + }
- {statsResponse.download_speed.human_readable} + {statsResponse.live?.download_speed.human_readable ?? "N/A"} {getCompletionETA(statsResponse)} - {`${statsResponse.snapshot.peer_stats.live} / ${statsResponse.snapshot.peer_stats.seen}`} + {getPeersString(statsResponse)} : } @@ -374,7 +399,7 @@ const RootContent = (props: { closeableError: ErrorDetails, otherError: ErrorDet }; function torrentIsDone(stats: TorrentStats): boolean { - return stats.snapshot.have_bytes == stats.snapshot.total_bytes; + return stats.finished; } function formatBytes(bytes: number): string { @@ -398,11 +423,11 @@ function getLargestFileName(torrentDetails: TorrentDetails): string { } function getCompletionETA(stats: TorrentStats): string { - if (stats.time_remaining && stats.time_remaining.duration) { - return formatSecondsToTime(stats.time_remaining.duration.secs); - } else { + let duration = stats?.live?.time_remaining?.duration?.secs; + if (duration == null) { return 'N/A'; } + return formatSecondsToTime(duration); } function formatSecondsToTime(seconds: number): string { diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index c46d0e2..e1b923b 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -238,12 +238,12 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> let stats_printer = |session: Arc| async move { loop { session.with_torrents(|torrents| { - for (idx, torrent) in torrents.iter().enumerate() { + for (idx, torrent) in torrents { let live = torrent.with_state(|s| { match s { ManagedTorrentState::Initializing(_) => info!("[{}] initializing", idx), ManagedTorrentState::Live(h) => return Some(h.clone()), - ManagedTorrentState::Error(_) | ManagedTorrentState::Paused(_) => {}, + _ => {}, }; None }); @@ -397,10 +397,11 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> .await { Ok(v) => match v { - AddTorrentResponse::AlreadyManaged(handle) => { + AddTorrentResponse::AlreadyManaged(id, handle) => { info!( - "torrent {:?} is already managed, downloaded to {:?}", + "torrent {:?} is already managed, id={}, downloaded to {:?}", handle.info_hash(), + id, handle.info().out_dir ); continue; @@ -426,7 +427,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> } continue; } - AddTorrentResponse::Added(handle) => { + AddTorrentResponse::Added(_, handle) => { added = true; handle } @@ -437,7 +438,6 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> } }; - http_api.add_torrent_handle(handle.clone()); handles.push(handle); } From c2dd36779491d4fdbefdb423f5465adbed90b1db Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 15:47:51 +0000 Subject: [PATCH 16/38] UI supporting multiple states --- crates/librqbit/webui/src/index.tsx | 49 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 40b219f..c938f00 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -19,15 +19,16 @@ const TorrentRow: React.FC<{ id: number, detailsResponse: TorrentDetails, statsResponse: TorrentStats }> = ({ id, detailsResponse, statsResponse }) => { const state = statsResponse?.state ?? ""; - + const error = statsResponse?.error; const totalBytes = statsResponse?.total_bytes ?? 1; const progressBytes = statsResponse?.progress_bytes ?? 0; const finished = statsResponse?.finished || false; - const progressPercentage = state == 'error' ? 100 : (progressBytes / totalBytes) * 100; + const progressPercentage = error ? 100 : (progressBytes / totalBytes) * 100; const isAnimated = (state == "initializing" || state == "live") && !finished; - const progressLabel = state == 'error' ? 'Error' : `${progressPercentage.toFixed(2)}%`; + const progressLabel = error ? 'Error' : `${progressPercentage.toFixed(2)}%`; + const progressBarVariant = error ? 'danger' : finished ? 'success' : 'info'; - const getPeersString = (statsResponse: TorrentStats) => { + const formatPeersString = () => { let peer_stats = statsResponse?.live?.snapshot.peer_stats; if (!peer_stats) { return ''; @@ -35,37 +36,47 @@ const TorrentRow: React.FC<{ return `${peer_stats.live} / ${peer_stats.seen}`; } - let classNames = []; - if (id % 2 == 0) { - classNames.push('bg-light'); + const formatDownloadSped = () => { + if (finished) { + return 'Completed'; + } + if (state == 'initializing') { + return 'Checking files'; + } + return statsResponse.live?.download_speed.human_readable ?? "N/A"; } - if (statsResponse?.error) { + + let classNames = []; + + if (error) { classNames.push('bg-warning'); + } else { + if (id % 2 == 0) { + classNames.push('bg-light'); + } } return ( {detailsResponse ? -
- {getLargestFileName(detailsResponse)} -
+ <> +
+ {getLargestFileName(detailsResponse)} +
+ {error &&

Error: {error}

} + : }
{statsResponse ? <> {`${formatBytes(totalBytes)} `} - - { - statsResponse.error && ( -

{statsResponse.error}

- ) - } +
- {statsResponse.live?.download_speed.human_readable ?? "N/A"} + {formatDownloadSped()} {getCompletionETA(statsResponse)} - {getPeersString(statsResponse)} + {formatPeersString()} : } From 0b8580dacdb945421b1680c98792b6bf39f64ad7 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 16:32:39 +0000 Subject: [PATCH 17/38] Torrent actions UI fully working (backend not yet) --- crates/librqbit/webui/index.html | 2 + crates/librqbit/webui/src/api.ts | 7 +- crates/librqbit/webui/src/index.tsx | 142 +++++++++++++++++++++++++++- 3 files changed, 145 insertions(+), 6 deletions(-) diff --git a/crates/librqbit/webui/index.html b/crates/librqbit/webui/index.html index 29217f0..2e09371 100644 --- a/crates/librqbit/webui/index.html +++ b/crates/librqbit/webui/index.html @@ -8,6 +8,8 @@ + diff --git a/crates/librqbit/webui/src/api.ts b/crates/librqbit/webui/src/api.ts index e99026f..eba0f63 100644 --- a/crates/librqbit/webui/src/api.ts +++ b/crates/librqbit/webui/src/api.ts @@ -69,8 +69,13 @@ export interface LiveTorrentStats { } | null; } +export const STATE_INITIALIZING = 'initializing'; +export const STATE_PAUSED = 'paused'; +export const STATE_LIVE = 'live'; +export const STATE_ERROR = 'error'; + export interface TorrentStats { - state: string, + state: 'initializing' | 'paused' | 'live' | 'error', error: string | null, progress_bytes: number, finished: boolean, diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index c938f00..efdb1b8 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -1,7 +1,7 @@ -import { StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react'; +import { MouseEventHandler, StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner, Table } from 'react-bootstrap'; -import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API } from './api'; +import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE } from './api'; interface Error { text: string, @@ -15,6 +15,135 @@ interface ContextType { const AppContext = createContext(null); +const IconButton: React.FC<{ + className: string, + onClick: () => void, + disabled?: boolean, + color?: string, +}> = ({ className, onClick, disabled, color }) => { + const onClickStopPropagation = (e) => { + e.stopPropagation(); + if (disabled) { + return; + } + onClick(); + } + return +} + +const DeleteTorrentModal = ({ id, show, onHide }) => { + if (!show) { + return null; + } + const [deleteFiles, setDeleteFiles] = useState(false); + const [error, setError] = useState(null); + const [deleting, setDeleting] = useState(false); + + const close = () => { + setDeleteFiles(false); + setError(null); + setDeleting(false); + onHide(); + } + + const deleteTorrent = () => { + setDeleting(true); + + const call = deleteFiles ? API.delete : API.forget; + + call(id).then(() => { + close(); + }).catch((e) => { + setError({ + text: `Error deleting torrent id=${id}`, + details: e, + }); + setDeleting(false); + }) + } + + return + + Delete torrent + + +
+ + setDeleteFiles(!deleteFiles)}> + + +
+ {error && } +
+ + {deleting && } + + + +
+} + +const TorrentActions: React.FC<{ + id: number, statsResponse: TorrentStats +}> = ({ id, statsResponse }) => { + let state = statsResponse.state; + + let [disabled, setDisabled] = useState(false); + let [deleting, setDeleting] = useState(false); + + const canPause = state == 'live'; + const canUnpause = state == 'paused'; + + const ctx = useContext(AppContext); + + const unpause = () => { + setDisabled(true); + API.start(id).finally(() => setDisabled(false)).catch((e) => { + ctx.setCloseableError({ + text: `Error starting torrent id=${id}`, + details: e, + }); + }) + }; + + const pause = () => { + setDisabled(true); + API.pause(id).finally(() => setDisabled(false)).catch((e) => { + ctx.setCloseableError({ + text: `Error pausing torrent id=${id}`, + details: e, + }); + }) + }; + + const startDeleting = () => { + setDisabled(true); + setDeleting(true); + } + + const cancelDeleting = () => { + setDisabled(false); + setDeleting(false); + } + + return + + {canUnpause && } + {canPause && } + + + + +} + const TorrentRow: React.FC<{ id: number, detailsResponse: TorrentDetails, statsResponse: TorrentStats }> = ({ id, detailsResponse, statsResponse }) => { @@ -24,7 +153,7 @@ const TorrentRow: React.FC<{ const progressBytes = statsResponse?.progress_bytes ?? 0; const finished = statsResponse?.finished || false; const progressPercentage = error ? 100 : (progressBytes / totalBytes) * 100; - const isAnimated = (state == "initializing" || state == "live") && !finished; + const isAnimated = (state == STATE_INITIALIZING || state == STATE_LIVE) && !finished; const progressLabel = error ? 'Error' : `${progressPercentage.toFixed(2)}%`; const progressBarVariant = error ? 'danger' : finished ? 'success' : 'info'; @@ -40,7 +169,7 @@ const TorrentRow: React.FC<{ if (finished) { return 'Completed'; } - if (state == 'initializing') { + if (state == STATE_INITIALIZING) { return 'Checking files'; } return statsResponse.live?.download_speed.human_readable ?? "N/A"; @@ -58,7 +187,7 @@ const TorrentRow: React.FC<{ return ( - + {detailsResponse ? <>
@@ -77,6 +206,9 @@ const TorrentRow: React.FC<{ {formatDownloadSped()} {getCompletionETA(statsResponse)} {formatPeersString()} + + + : } From d7a37c1b48dbd00f578fd32435096d1627df04e9 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 18:28:46 +0000 Subject: [PATCH 18/38] Saving --- crates/librqbit/src/http_api.rs | 47 ++- crates/librqbit/src/http_api_error.rs | 8 + crates/librqbit/src/peer_connection.rs | 16 +- crates/librqbit/src/peer_info_reader/mod.rs | 4 +- crates/librqbit/src/session.rs | 7 +- crates/librqbit/src/spawn_utils.rs | 8 +- .../src/torrent_state/initializing.rs | 5 + crates/librqbit/src/torrent_state/live/mod.rs | 277 ++++++++++++------ crates/librqbit/src/torrent_state/mod.rs | 83 +++--- crates/librqbit/webui/src/index.tsx | 43 ++- crates/rqbit/src/main.rs | 7 +- 11 files changed, 337 insertions(+), 168 deletions(-) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 2d4edc1..3c227f0 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -127,14 +127,28 @@ impl HttpApi { State(state): State, Path(idx): Path, ) -> Result { - state.api_torrent_action_pause(idx) + state.api_torrent_action_pause(idx).map(axum::Json) } async fn torrent_action_start( State(state): State, Path(idx): Path, ) -> Result { - state.api_torrent_action_start(idx) + state.api_torrent_action_start(idx).map(axum::Json) + } + + async fn torrent_action_forget( + State(state): State, + Path(idx): Path, + ) -> Result { + state.api_torrent_action_forget(idx).map(axum::Json) + } + + async fn torrent_action_delete( + State(state): State, + Path(idx): Path, + ) -> Result { + state.api_torrent_action_delete(idx).map(axum::Json) } #[allow(unused_mut)] @@ -149,7 +163,9 @@ impl HttpApi { .route("/torrents/:id/stats/v1", get(torrent_stats_v1)) .route("/torrents/:id/peer_stats", get(peer_stats)) .route("/torrents/:id/pause", post(torrent_action_pause)) - .route("/torrents/:id/start", post(torrent_action_start)); + .route("/torrents/:id/start", post(torrent_action_start)) + .route("/torrents/:id/forget", post(torrent_action_forget)) + .route("/torrents/:id/delete", post(torrent_action_delete)); #[cfg(feature = "webui")] { @@ -244,6 +260,9 @@ pub struct TorrentDetailsResponseFile { pub included: bool, } +#[derive(Default, Serialize)] +struct EmptyJsonResponse {} + #[derive(Serialize, Deserialize)] pub struct TorrentDetailsResponse { pub info_hash: String, @@ -409,20 +428,32 @@ impl ApiInternal { .per_peer_stats_snapshot(filter)) } - fn api_torrent_action_pause(&self, idx: TorrentId) -> Result<()> { + fn api_torrent_action_pause(&self, idx: TorrentId) -> Result { let handle = self.mgr_handle(idx)?; handle .pause() .context("error pausing torrent") - .with_error_status_code(StatusCode::BAD_REQUEST) + .with_error_status_code(StatusCode::BAD_REQUEST)?; + Ok(Default::default()) } - fn api_torrent_action_start(&self, idx: TorrentId) -> Result<()> { + fn api_torrent_action_start(&self, idx: TorrentId) -> Result { let handle = self.mgr_handle(idx)?; self.session .unpause(&handle) .context("error unpausing torrent") - .with_error_status_code(StatusCode::BAD_REQUEST) + .with_error_status_code(StatusCode::BAD_REQUEST)?; + Ok(Default::default()) + } + + fn api_torrent_action_forget(&self, idx: TorrentId) -> Result { + Err(ApiError::not_implemented("forgetting not implemented yet")) + } + + fn api_torrent_action_delete(&self, idx: TorrentId) -> Result { + Err(ApiError::not_implemented( + "deleting torrent not implemented yet", + )) } pub async fn api_add_torrent( @@ -528,7 +559,7 @@ impl ApiInternal { ManagedTorrentState::Paused(p) => { resp.state = "paused"; resp.progress_bytes = p.have_bytes; - resp.finished = p.have_bytes == resp.progress_bytes; + resp.finished = p.have_bytes == resp.total_bytes; } ManagedTorrentState::Live(l) => { resp.state = "live"; diff --git a/crates/librqbit/src/http_api_error.rs b/crates/librqbit/src/http_api_error.rs index dfa68db..628eb34 100644 --- a/crates/librqbit/src/http_api_error.rs +++ b/crates/librqbit/src/http_api_error.rs @@ -19,6 +19,14 @@ impl ApiError { } } + pub fn not_implemented(msg: &str) -> Self { + Self { + status: Some(StatusCode::INTERNAL_SERVER_ERROR), + kind: ApiErrorKind::Other(anyhow::anyhow!("{}", msg)), + plaintext: false, + } + } + pub const fn dht_disabled() -> Self { Self { status: Some(StatusCode::NOT_FOUND), diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index cd8ca38..289e061 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -20,7 +20,7 @@ use crate::spawn_utils::BlockingSpawner; pub trait PeerConnectionHandler { fn on_connected(&self, _connection_time: Duration) {} fn get_have_bytes(&self) -> u64; - fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec) -> Option; + fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec) -> anyhow::Result; fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()>; fn on_extended_handshake( &self, @@ -204,15 +204,13 @@ impl PeerConnection { .unwrap_or_else(|| Duration::from_secs(120)); if self.handler.get_have_bytes() > 0 { - if let Some(len) = self + let len = self .handler - .serialize_bitfield_message_to_buf(&mut write_buf) - { - with_timeout(rwtimeout, write_half.write_all(&write_buf[..len])) - .await - .context("error writing bitfield to peer")?; - debug!("sent bitfield"); - } + .serialize_bitfield_message_to_buf(&mut write_buf)?; + with_timeout(rwtimeout, write_half.write_all(&write_buf[..len])) + .await + .context("error writing bitfield to peer")?; + debug!("sent bitfield"); } loop { diff --git a/crates/librqbit/src/peer_info_reader/mod.rs b/crates/librqbit/src/peer_info_reader/mod.rs index fca366c..a205a0d 100644 --- a/crates/librqbit/src/peer_info_reader/mod.rs +++ b/crates/librqbit/src/peer_info_reader/mod.rs @@ -141,8 +141,8 @@ impl PeerConnectionHandler for Handler { 0 } - fn serialize_bitfield_message_to_buf(&self, _buf: &mut Vec) -> Option { - None + fn serialize_bitfield_message_to_buf(&self, _buf: &mut Vec) -> anyhow::Result { + Ok(0) } fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> { diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 2d1d656..e073a84 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -11,7 +11,7 @@ use librqbit_core::{ use parking_lot::RwLock; use reqwest::Url; use tokio_stream::StreamExt; -use tracing::{debug, info, trace_span, warn}; +use tracing::{debug, error_span, info, warn}; use crate::{ dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult}, @@ -412,13 +412,14 @@ impl Session { { return Ok(AddTorrentResponse::AlreadyManaged(id, handle.clone())); } - let managed_torrent = builder.build()?; + let next_id = g.torrents.len(); + let managed_torrent = builder.build(error_span!("torrent", id = next_id))?; let id = g.add_torrent(managed_torrent.clone()); (managed_torrent, id) }; { - let span = trace_span!("torrent", id = id); + let span = managed_torrent.info.span.clone(); let _ = span.enter(); managed_torrent .start(initial_peers, dht_peer_rx) diff --git a/crates/librqbit/src/spawn_utils.rs b/crates/librqbit/src/spawn_utils.rs index e108254..3b54967 100644 --- a/crates/librqbit/src/spawn_utils.rs +++ b/crates/librqbit/src/spawn_utils.rs @@ -1,9 +1,9 @@ -use tracing::{debug, error, trace, Instrument}; +use tracing::{debug, trace, Instrument}; pub fn spawn( span: tracing::Span, fut: impl std::future::Future> + Send + 'static, -) { +) -> tokio::task::JoinHandle<()> { let fut = async move { trace!("started"); match fut.await { @@ -11,12 +11,12 @@ pub fn spawn( debug!("finished"); } Err(e) => { - error!("{:#}", e) + debug!("finished with error: {:#}", e) } } } .instrument(span.or_current()); - tokio::spawn(fut); + tokio::spawn(fut) } #[derive(Clone, Copy, Debug)] diff --git a/crates/librqbit/src/torrent_state/initializing.rs b/crates/librqbit/src/torrent_state/initializing.rs index c40d2db..d9d2ae0 100644 --- a/crates/librqbit/src/torrent_state/initializing.rs +++ b/crates/librqbit/src/torrent_state/initializing.rs @@ -35,6 +35,11 @@ impl TorrentStateInitializing { } } + pub fn get_checked_bytes(&self) -> u64 { + self.checked_bytes + .load(std::sync::atomic::Ordering::Relaxed) + } + pub async fn check(&self) -> anyhow::Result { let (files, filenames) = { let mut files = diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 68a96c8..ac9f295 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -61,6 +61,7 @@ use bencode::from_bytes; use buffers::{ByteBuf, ByteString}; use clone_to_owned::CloneToOwned; use futures::{stream::FuturesUnordered, StreamExt}; +use itertools::Itertools; use librqbit_core::{ id20::Id20, lengths::{ChunkInfo, Lengths, ValidPieceIndex}, @@ -79,7 +80,7 @@ use tokio::{ }, time::timeout, }; -use tracing::{debug, error, info, span, trace, trace_span, warn, Level}; +use tracing::{debug, error, error_span, info, trace, warn}; use url::Url; use crate::{ @@ -116,15 +117,42 @@ struct InflightPiece { started: Instant, } +fn dummy_file() -> anyhow::Result { + #[cfg(target_os = "windows")] + const DEVNULL: &str = "NUL"; + #[cfg(not(target_os = "windows"))] + const DEVNULL: &str = "/dev/null"; + + std::fs::OpenOptions::new() + .read(true) + .open(DEVNULL) + .with_context(|| format!("error opening {}", DEVNULL)) +} + pub(crate) struct TorrentStateLocked { // What chunks we have and need. - pub(crate) chunks: ChunkTracker, + // If this is None, the torrent was paused, and this live state is useless, and needs to be dropped. + pub(crate) chunks: Option, // At a moment in time, we are expecting a piece from only one peer. // inflight_pieces stores this information. inflight_pieces: HashMap, } +impl TorrentStateLocked { + pub(crate) fn get_chunks(&self) -> anyhow::Result<&ChunkTracker> { + self.chunks + .as_ref() + .context("chunk tracker empty, torrent was paused") + } + + fn get_chunks_mut(&mut self) -> anyhow::Result<&mut ChunkTracker> { + self.chunks + .as_mut() + .context("chunk tracker empty, torrent was paused") + } +} + #[derive(Default)] pub struct TorrentStateOptions { pub peer_connect_timeout: Option, @@ -134,7 +162,8 @@ pub struct TorrentStateOptions { pub struct TorrentStateLive { peers: PeerStates, meta: Arc, - locked: Arc>, + locked: RwLock, + files: Vec>>, filenames: Vec, @@ -153,6 +182,9 @@ pub struct TorrentStateLive { finished_notify: Notify, + cancel_tx: tokio::sync::watch::Sender<()>, + cancel_rx: tokio::sync::watch::Receiver<()>, + speed_estimator: SpeedEstimator, } @@ -166,13 +198,15 @@ impl TorrentStateLive { let needed_bytes = paused.info.lengths.total_length() - have_bytes; let lengths = *paused.chunk_tracker.get_lengths(); + let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(()); + let state = Arc::new(TorrentStateLive { meta: paused.info.clone(), peers: Default::default(), - locked: Arc::new(RwLock::new(TorrentStateLocked { - chunks: paused.chunk_tracker, + locked: RwLock::new(TorrentStateLocked { + chunks: Some(paused.chunk_tracker), inflight_pieces: Default::default(), - })), + }), files: paused.files, filenames: paused.filenames, stats: AtomicStats { @@ -182,46 +216,71 @@ impl TorrentStateLive { needed_bytes, have_plus_needed_bytes: needed_bytes + have_bytes, lengths, - peer_semaphore: Semaphore::new(128), peer_queue_tx, finished_notify: Notify::new(), speed_estimator, + cancel_rx, + cancel_tx, }); for tracker in state.meta.trackers.iter() { - spawn( - trace_span!("tracker_monitor", url = tracker.to_string()), + state.spawn( + error_span!(parent: state.meta.span.clone(), "tracker_monitor", url = tracker.to_string()), state.clone().task_single_tracker_monitor(tracker.clone()), ); } - spawn(span!(Level::ERROR, "speed_estimator_updater"), { - let state = state.clone(); - async move { - loop { - let stats = state.stats_snapshot(); - let fetched = stats.fetched_bytes; - let needed = state.initially_needed(); - // fetched can be too high in theory, so for safety make sure that it doesn't wrap around u64. - let remaining = needed - .wrapping_sub(fetched) - .min(needed - stats.downloaded_and_checked_bytes); - state - .speed_estimator - .add_snapshot(fetched, remaining, Instant::now()); - tokio::time::sleep(Duration::from_secs(1)).await; + state.spawn( + error_span!(parent: state.meta.span.clone(), "speed_estimator_updater"), + { + let state = Arc::downgrade(&state); + async move { + loop { + let state = match state.upgrade() { + Some(state) => state, + None => return Ok(()), + }; + let stats = state.stats_snapshot(); + let fetched = stats.fetched_bytes; + let needed = state.initially_needed(); + // fetched can be too high in theory, so for safety make sure that it doesn't wrap around u64. + let remaining = needed + .wrapping_sub(fetched) + .min(needed - stats.downloaded_and_checked_bytes); + state + .speed_estimator + .add_snapshot(fetched, remaining, Instant::now()); + tokio::time::sleep(Duration::from_secs(1)).await; + } } - } - }); + }, + ); - spawn( - span!(Level::ERROR, "peer_adder"), + state.spawn( + error_span!(parent: state.meta.span.clone(), "peer_adder"), state.clone().task_peer_adder(peer_queue_rx), ); state } + fn spawn( + &self, + span: tracing::Span, + fut: impl std::future::Future> + Send + 'static, + ) { + let mut cancel_rx = self.cancel_rx.clone(); + spawn(span, async move { + tokio::select! { + r = fut => r, + _ = cancel_rx.changed() => { + error!("canceled"); + bail!("canceled") + } + } + }); + } + pub fn speed_estimator(&self) -> &SpeedEstimator { &self.speed_estimator } @@ -241,7 +300,7 @@ impl TorrentStateLive { let response = from_bytes::(&bytes)?; for peer in response.peers.iter_sockaddrs() { - self.add_peer_if_not_seen(peer); + self.add_peer_if_not_seen(peer)?; } Ok(response.interval) } @@ -345,11 +404,11 @@ impl TorrentStateLive { match res { // We disconnected the peer ourselves as we don't need it Ok(()) => { - handler.on_peer_died(None); + handler.on_peer_died(None)?; } Err(e) => { debug!("error managing peer: {:#}", e); - handler.on_peer_died(Some(e)); + handler.on_peer_died(Some(e))?; } } Ok::<_, anyhow::Error>(()) @@ -361,17 +420,17 @@ impl TorrentStateLive { ) -> anyhow::Result<()> { let state = self; loop { - let addr = peer_queue_rx.recv().await.unwrap(); + let addr = peer_queue_rx.recv().await.context("torrent closed")?; if state.is_finished() { debug!("ignoring peer {} as we are finished", addr); state.peers.mark_peer_not_needed(addr); continue; } - let permit = state.peer_semaphore.acquire().await.unwrap(); + let permit = state.peer_semaphore.acquire().await?; permit.forget(); - spawn( - span!(parent: None, Level::ERROR, "manage_peer", peer = addr.to_string()), + state.spawn( + error_span!(parent: state.meta.span.clone(), "manage_peer", peer = addr.to_string()), state.clone().task_manage_peer(addr), ); } @@ -406,23 +465,28 @@ impl TorrentStateLive { TimedExistence::new(timeit(reason, || self.locked.write()), reason) } - fn get_next_needed_piece(&self, peer_handle: PeerHandle) -> Option { + fn get_next_needed_piece( + &self, + peer_handle: PeerHandle, + ) -> anyhow::Result> { self.peers .with_live_mut(peer_handle, "l(get_next_needed_piece)", |live| { let g = self.lock_read("g(get_next_needed_piece)"); let bf = &live.bitfield; - for n in g.chunks.iter_needed_pieces() { + for n in g.get_chunks()?.iter_needed_pieces() { if bf.get(n).map(|v| *v) == Some(true) { // in theory it should be safe without validation, but whatever. - return self.lengths.validate_piece_index(n as u32); + return Ok(self.lengths.validate_piece_index(n as u32)); } } - None - })? + Ok(None) + }) + .transpose() + .map(|r| r.flatten()) } fn am_i_interested_in_peer(&self, handle: PeerHandle) -> bool { - self.get_next_needed_piece(handle).is_some() + matches!(self.get_next_needed_piece(handle), Ok(Some(_))) } fn set_peer_live(&self, handle: PeerHandle, h: Handshake) { @@ -449,6 +513,10 @@ impl TorrentStateLive { .load(Ordering::Acquire) } + pub fn get_have_bytes(&self) -> u64 { + self.stats.have_bytes.load(Ordering::Relaxed) + } + pub fn is_finished(&self) -> bool { self.get_left_to_download_bytes() == 0 } @@ -498,9 +566,11 @@ impl TorrentStateLive { } let mut unordered: FuturesUnordered<_> = futures.into_iter().collect(); - spawn( - span!( - Level::ERROR, + + // We don't want to remember this task as there may be too many. + self.spawn( + error_span!( + parent: self.meta.span.clone(), "transmit_haves", piece = index.get(), count = unordered.len() @@ -512,14 +582,14 @@ impl TorrentStateLive { ); } - pub(crate) fn add_peer_if_not_seen(&self, addr: SocketAddr) -> bool { + pub(crate) fn add_peer_if_not_seen(&self, addr: SocketAddr) -> anyhow::Result { match self.peers.add_if_not_seen(addr) { Some(handle) => handle, - None => return false, + None => return Ok(false), }; - self.peer_queue_tx.send(addr).unwrap(); - true + self.peer_queue_tx.send(addr)?; + Ok(true) } pub fn stats_snapshot(&self) -> StatsSnapshot { @@ -560,7 +630,34 @@ impl TorrentStateLive { } pub fn pause(&self) -> anyhow::Result { - bail!("pause not implemented yet") + let _ = self.cancel_tx.send(()); + + let mut g = self.locked.write(); + + let files = self + .files + .iter() + .map(|f| { + let mut f = f.lock(); + let dummy = dummy_file()?; + let f = std::mem::replace(&mut *f, dummy); + Ok::<_, anyhow::Error>(Arc::new(Mutex::new(f))) + }) + .try_collect()?; + + let filenames = self.filenames.clone(); + + // g.chunks; + Ok(TorrentStatePaused { + info: self.meta.clone(), + files, + filenames, + chunk_tracker: g + .chunks + .take() + .context("bug: pausing already paused torrent")?, + have_bytes: self.get_have_bytes(), + }) } } @@ -633,16 +730,12 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler { Ok(()) } - fn get_have_bytes(&self) -> u64 { - self.state.stats.have_bytes.load(Ordering::Relaxed) - } - - fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec) -> Option { + fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec) -> anyhow::Result { let g = self.state.lock_read("serialize_bitfield_message_to_buf"); - let msg = Message::Bitfield(ByteBuf(g.chunks.get_have_pieces().as_raw_slice())); - let len = msg.serialize(buf, None).unwrap(); + let msg = Message::Bitfield(ByteBuf(g.get_chunks()?.get_have_pieces().as_raw_slice())); + let len = msg.serialize(buf, None)?; debug!("sending: {:?}, length={}", &msg, len); - Some(len) + Ok(len) } fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> { @@ -664,10 +757,14 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler { fn on_extended_handshake(&self, _: &ExtendedHandshake) -> anyhow::Result<()> { Ok(()) } + + fn get_have_bytes(&self) -> u64 { + self.state.get_have_bytes() + } } impl PeerHandler { - fn on_peer_died(self, error: Option) { + fn on_peer_died(self, error: Option) -> anyhow::Result<()> { let peers = &self.state.peers; let pstats = &peers.stats; let handle = self.addr; @@ -675,7 +772,7 @@ impl PeerHandler { Some(peer) => TimedExistence::new(peer, "on_peer_died"), None => { warn!("bug: peer not found in table. Forgetting it forever"); - return; + return Ok(()); } }; let prev = pe.value_mut().state.take(pstats); @@ -690,20 +787,21 @@ impl PeerHandler { req.piece.get(), req.chunk ); - g.chunks.mark_chunk_request_cancelled(req.piece, req.chunk); + g.get_chunks_mut()? + .mark_chunk_request_cancelled(req.piece, req.chunk); } } PeerState::NotNeeded => { // Restore it as std::mem::take() replaced it above. pe.value_mut().state.set(PeerState::NotNeeded, pstats); - return; + return Ok(()); } s @ PeerState::Queued | s @ PeerState::Dead => { warn!("bug: peer was in a wrong state {s:?}, ignoring it forever"); // Prevent deadlocks. drop(pe); self.state.peers.drop_peer(handle); - return; + return Ok(()); } }; @@ -712,7 +810,7 @@ impl PeerHandler { None => { debug!("peer died without errors, not re-queueing"); pe.value_mut().state.set(PeerState::NotNeeded, pstats); - return; + return Ok(()); } }; @@ -721,7 +819,7 @@ impl PeerHandler { if self.state.is_finished() { debug!("torrent finished, not re-queueing"); pe.value_mut().state.set(PeerState::NotNeeded, pstats); - return; + return Ok(()); } pe.value_mut().state.set(PeerState::Dead, pstats); @@ -732,10 +830,9 @@ impl PeerHandler { drop(pe); if let Some(dur) = backoff { - spawn( - span!( - parent: None, - Level::ERROR, + self.state.clone().spawn( + error_span!( + parent: self.state.meta.span.clone(), "wait_for_peer", peer = handle.to_string(), duration = format!("{dur:?}") @@ -764,31 +861,35 @@ impl PeerHandler { } else { debug!("dropping peer, backoff exhausted"); self.state.peers.drop_peer(handle); - } + }; + Ok(()) } - fn reserve_next_needed_piece(&self) -> Option { + fn reserve_next_needed_piece(&self) -> anyhow::Result> { // TODO: locking one inside the other in different order results in deadlocks. self.state .peers .with_live_mut(self.addr, "reserve_next_needed_piece", |live| { if self.locked.read().i_am_choked { debug!("we are choked, can't reserve next piece"); - return None; + return Ok(None); } let mut g = self.state.lock_write("reserve_next_needed_piece"); let n = { let mut n_opt = None; let bf = &live.bitfield; - for n in g.chunks.iter_needed_pieces() { + for n in g.get_chunks()?.iter_needed_pieces() { if bf.get(n).map(|v| *v) == Some(true) { n_opt = Some(n); break; } } - self.state.lengths.validate_piece_index(n_opt? as u32)? + self.state + .lengths + .validate_piece_index(n_opt.context("invalid n_opt")? as u32) + .context("invalid piece")? }; g.inflight_pieces.insert( n, @@ -797,10 +898,11 @@ impl PeerHandler { started: Instant::now(), }, ); - g.chunks.reserve_needed_piece(n); - Some(n) + g.get_chunks_mut()?.reserve_needed_piece(n); + Ok(Some(n)) }) - .flatten() + .transpose() + .map(|r| r.flatten()) } fn try_steal_old_slow_piece(&self, threshold: f64) -> Option { @@ -865,7 +967,7 @@ impl PeerHandler { if !self .state .lock_read("is_chunk_ready_to_upload") - .chunks + .get_chunks()? .is_chunk_ready_to_upload(&chunk_info) { anyhow::bail!( @@ -965,7 +1067,7 @@ impl PeerHandler { // Afterwards means we are close to completion, try stealing more aggressively. let next = match self .try_steal_old_slow_piece(10.) - .or_else(|| self.reserve_next_needed_piece()) + .or_else(|| self.reserve_next_needed_piece().ok().flatten()) .or_else(|| self.try_steal_old_slow_piece(2.)) { Some(next) => next, @@ -1037,18 +1139,6 @@ impl PeerHandler { } fn reopen_read_only(&self) -> anyhow::Result<()> { - fn dummy_file() -> anyhow::Result { - #[cfg(target_os = "windows")] - const DEVNULL: &str = "NUL"; - #[cfg(not(target_os = "windows"))] - const DEVNULL: &str = "/dev/null"; - - std::fs::OpenOptions::new() - .read(true) - .open(DEVNULL) - .with_context(|| format!("error opening {}", DEVNULL)) - } - // Lock exclusive just in case to ensure in-flight operations finish.?? let _guard = self.state.lock_write("reopen_read_only"); @@ -1140,7 +1230,7 @@ impl PeerHandler { } }; - match g.chunks.mark_chunk_downloaded(&piece) { + match g.get_chunks_mut()?.mark_chunk_downloaded(&piece) { Some(ChunkMarkingResult::Completed) => { debug!("piece={} done, will write and checksum", piece.index,); // This will prevent others from stealing it. @@ -1204,7 +1294,8 @@ impl PeerHandler { true => { { let mut g = self.state.lock_write("mark_piece_downloaded"); - g.chunks.mark_piece_downloaded(chunk_info.piece_index); + g.get_chunks_mut()? + .mark_piece_downloaded(chunk_info.piece_index); } // Global piece counters. @@ -1256,7 +1347,7 @@ impl PeerHandler { warn!("checksum for piece={} did not validate", index,); self.state .lock_write("mark_piece_broken") - .chunks + .get_chunks_mut()? .mark_piece_broken(chunk_info.piece_index); } }; diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index a81a59f..e2637e1 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -21,7 +21,7 @@ pub use live::*; use parking_lot::RwLock; use tokio_stream::StreamExt; -use tracing::trace_span; +use tracing::error_span; use url::Url; use crate::chunk_tracker::ChunkTracker; @@ -75,6 +75,7 @@ pub struct ManagedTorrentInfo { pub trackers: Vec, pub peer_id: Id20, pub lengths: Lengths, + pub span: tracing::Span, pub(crate) options: ManagedTorrentOptions, } @@ -89,6 +90,10 @@ impl ManagedTorrent { &self.info } + pub fn get_total_bytes(&self) -> u64 { + self.info.lengths.total_length() + } + pub fn info_hash(&self) -> Id20 { self.info.info_hash } @@ -105,7 +110,7 @@ impl ManagedTorrent { let g = self.locked.read(); match &g.state { ManagedTorrentState::Paused(p) => Ok(f(&p.chunk_tracker)), - ManagedTorrentState::Live(l) => Ok(f(&l.lock_read("chunk_tracker").chunks)), + ManagedTorrentState::Live(l) => Ok(f(l.lock_read("chunk_tracker").get_chunks()?)), _ => bail!("no chunk tracker, torrent neither paused nor live"), } } @@ -131,42 +136,51 @@ impl ManagedTorrent { ManagedTorrentState::Initializing(init) => { let init = init.clone(); let t = self.clone(); - spawn(trace_span!("initialize_and_start"), async move { - match init.check().await { - Ok(paused) => { - let live = TorrentStateLive::new(paused); - t.locked.write().state = ManagedTorrentState::Live(live.clone()); + let span = self.info.span.clone(); + spawn( + error_span!(parent: span.clone(), "initialize_and_start"), + async move { + match init.check().await { + Ok(paused) => { + let live = TorrentStateLive::new(paused); + t.locked.write().state = ManagedTorrentState::Live(live.clone()); - let live = Arc::downgrade(&live); - spawn(trace_span!("peer_adder"), async move { - { - let live: Arc = - live.upgrade().context("no longer live")?; - for peer in initial_peers { - live.add_peer_if_not_seen(peer); - } - } + let live = Arc::downgrade(&live); + spawn( + error_span!(parent: span.clone(), "external_peer_adder"), + async move { + { + let live: Arc = + live.upgrade().context("no longer live")?; + for peer in initial_peers { + live.add_peer_if_not_seen(peer) + .context("torrent closed")?; + } + } - if let Some(mut peer_rx) = peer_rx { - while let Some(peer) = peer_rx.next().await { - live.upgrade() - .context("no longer live")? - .add_peer_if_not_seen(peer); - } - } + if let Some(mut peer_rx) = peer_rx { + while let Some(peer) = peer_rx.next().await { + live.upgrade() + .context("no longer live")? + .add_peer_if_not_seen(peer) + .context("torrent closed")?; + } + } + + Ok(()) + }, + ); Ok(()) - }); - - Ok(()) + } + Err(err) => { + let result = anyhow::anyhow!("{:?}", err); + t.locked.write().state = ManagedTorrentState::Error(err); + Err(result) + } } - Err(err) => { - let result = anyhow::anyhow!("{:?}", err); - t.locked.write().state = ManagedTorrentState::Error(err); - Err(result) - } - } - }); + }, + ); Ok(()) } ManagedTorrentState::Paused(_) => { @@ -288,9 +302,10 @@ impl ManagedTorrentBuilder { self } - pub(crate) fn build(self) -> anyhow::Result { + pub(crate) fn build(self, span: tracing::Span) -> anyhow::Result { let lengths = Lengths::from_torrent(&self.info)?; let info = Arc::new(ManagedTorrentInfo { + span, info: self.info, info_hash: self.info_hash, out_dir: self.output_folder, diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index efdb1b8..57d2829 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -14,6 +14,7 @@ interface ContextType { } const AppContext = createContext(null); +const RefreshTorrentStatsContext = createContext<{ refresh: () => void }>(null); const IconButton: React.FC<{ className: string, @@ -39,6 +40,8 @@ const DeleteTorrentModal = ({ id, show, onHide }) => { const [error, setError] = useState(null); const [deleting, setDeleting] = useState(false); + const refreshCtx = useContext(RefreshTorrentStatsContext); + const close = () => { setDeleteFiles(false); setError(null); @@ -52,6 +55,7 @@ const DeleteTorrentModal = ({ id, show, onHide }) => { const call = deleteFiles ? API.delete : API.forget; call(id).then(() => { + refreshCtx.refresh(); close(); }).catch((e) => { setError({ @@ -99,6 +103,8 @@ const TorrentActions: React.FC<{ let [disabled, setDisabled] = useState(false); let [deleting, setDeleting] = useState(false); + let refreshCtx = useContext(RefreshTorrentStatsContext); + const canPause = state == 'live'; const canUnpause = state == 'paused'; @@ -106,22 +112,22 @@ const TorrentActions: React.FC<{ const unpause = () => { setDisabled(true); - API.start(id).finally(() => setDisabled(false)).catch((e) => { + API.start(id).then(() => { refreshCtx.refresh() }, (e) => { ctx.setCloseableError({ text: `Error starting torrent id=${id}`, details: e, }); - }) + }).finally(() => setDisabled(false)) }; const pause = () => { setDisabled(true); - API.pause(id).finally(() => setDisabled(false)).catch((e) => { + API.pause(id).then(() => { refreshCtx.refresh() }, (e) => { ctx.setCloseableError({ text: `Error pausing torrent id=${id}`, details: e, }); - }) + }).finally(() => setDisabled(false)) }; const startDeleting = () => { @@ -241,21 +247,34 @@ const Torrent = ({ id, torrent }) => { } }, [detailsResponse]); + const refreshStats = () => API.getTorrentStats(torrent.id).then((stats) => { + updateStatsResponse(stats); + return stats; + }); + // Update stats once then forever. useEffect(() => customSetInterval((async () => { const errorInterval = 10000; - const liveInterval = 500; - const finishedInterval = 5000; + const liveInterval = 1000; + const finishedInterval = 10000; + const nonLiveInterval = 10000; - return API.getTorrentStats(torrent.id).then((stats) => { - updateStatsResponse(stats); - return torrentIsDone(stats) ? finishedInterval : liveInterval; + return refreshStats().then((stats) => { + if (stats.finished) { + return finishedInterval; + } + if (stats.state == STATE_INITIALIZING || stats.state == STATE_LIVE) { + return liveInterval; + } + return nonLiveInterval; }, (e) => { return errorInterval; }); }), 0), []); - return + return + + } const TorrentsList = (props: { torrents: Array, loading: boolean }) => { @@ -541,10 +560,6 @@ const RootContent = (props: { closeableError: ErrorDetails, otherError: ErrorDet }; -function torrentIsDone(stats: TorrentStats): boolean { - return stats.finished; -} - function formatBytes(bytes: number): string { if (bytes === 0) return '0 Bytes'; diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index e1b923b..be85289 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -241,7 +241,12 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> for (idx, torrent) in torrents { let live = torrent.with_state(|s| { match s { - ManagedTorrentState::Initializing(_) => info!("[{}] initializing", idx), + ManagedTorrentState::Initializing(i) => { + let total = torrent.get_total_bytes(); + let progress = i.get_checked_bytes(); + let pct = (progress as f64 / total as f64) * 100f64; + info!("[{}] initializing {:.2}%", idx, pct) + }, ManagedTorrentState::Live(h) => return Some(h.clone()), _ => {}, }; From 4927850ff9759f191e11f8d02291f6f72b134cdb Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 21:03:34 +0000 Subject: [PATCH 19/38] Fixed a bug when unpausing torrents --- crates/librqbit/src/torrent_state/live/mod.rs | 1 - crates/librqbit/src/torrent_state/mod.rs | 62 +++++++++++-------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index ac9f295..faa2955 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -274,7 +274,6 @@ impl TorrentStateLive { tokio::select! { r = fut => r, _ = cancel_rx.changed() => { - error!("canceled"); bail!("canceled") } } diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index e2637e1..793e8f3 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -7,6 +7,7 @@ use std::net::SocketAddr; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use std::sync::Weak; use std::time::Duration; use anyhow::bail; @@ -21,6 +22,7 @@ pub use live::*; use parking_lot::RwLock; use tokio_stream::StreamExt; +use tracing::error; use tracing::error_span; use url::Url; @@ -110,7 +112,10 @@ impl ManagedTorrent { let g = self.locked.read(); match &g.state { ManagedTorrentState::Paused(p) => Ok(f(&p.chunk_tracker)), - ManagedTorrentState::Live(l) => Ok(f(l.lock_read("chunk_tracker").get_chunks()?)), + ManagedTorrentState::Live(l) => Ok(f(l + .lock_read("chunk_tracker") + .get_chunks() + .context("error getting chunks")?)), _ => bail!("no chunk tracker, torrent neither paused nor live"), } } @@ -129,6 +134,31 @@ impl ManagedTorrent { peer_rx: Option + Unpin + Send + Sync + 'static>, ) -> anyhow::Result<()> { let mut g = self.locked.write(); + + let peer_adder = |live: Weak| async move { + { + let live: Arc = live.upgrade().context("no longer live")?; + for peer in initial_peers { + live.add_peer_if_not_seen(peer).context("torrent closed")?; + } + } + + if let Some(mut peer_rx) = peer_rx { + while let Some(peer) = peer_rx.next().await { + live.upgrade() + .context("no longer live")? + .add_peer_if_not_seen(peer) + .context("torrent closed")?; + } + } else { + error!("peer rx is not set"); + } + + Ok(()) + }; + + let span = self.info.span.clone(); + match &g.state { ManagedTorrentState::Live(_) => { bail!("torrent is already live"); @@ -136,7 +166,6 @@ impl ManagedTorrent { ManagedTorrentState::Initializing(init) => { let init = init.clone(); let t = self.clone(); - let span = self.info.span.clone(); spawn( error_span!(parent: span.clone(), "initialize_and_start"), async move { @@ -145,30 +174,9 @@ impl ManagedTorrent { let live = TorrentStateLive::new(paused); t.locked.write().state = ManagedTorrentState::Live(live.clone()); - let live = Arc::downgrade(&live); spawn( error_span!(parent: span.clone(), "external_peer_adder"), - async move { - { - let live: Arc = - live.upgrade().context("no longer live")?; - for peer in initial_peers { - live.add_peer_if_not_seen(peer) - .context("torrent closed")?; - } - } - - if let Some(mut peer_rx) = peer_rx { - while let Some(peer) = peer_rx.next().await { - live.upgrade() - .context("no longer live")? - .add_peer_if_not_seen(peer) - .context("torrent closed")?; - } - } - - Ok(()) - }, + peer_adder(Arc::downgrade(&live)), ); Ok(()) @@ -186,7 +194,11 @@ impl ManagedTorrent { ManagedTorrentState::Paused(_) => { let paused = g.state.take().assert_paused(); let live = TorrentStateLive::new(paused); - g.state = ManagedTorrentState::Live(live); + g.state = ManagedTorrentState::Live(live.clone()); + spawn( + error_span!(parent: span.clone(), "external_peer_adder"), + peer_adder(Arc::downgrade(&live)), + ); Ok(()) } ManagedTorrentState::Error(_) => { From f789be22c98f960dcdb24360b071cfc3efd751b5 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 21:45:38 +0000 Subject: [PATCH 20/38] Deleting and forgetting torrents --- crates/librqbit/src/http_api.rs | 8 +++--- crates/librqbit/src/session.rs | 42 ++++++++++++++++++++-------- crates/librqbit/webui/vite.config.ts | 1 - 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 3c227f0..f461384 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -447,13 +447,13 @@ impl ApiInternal { } fn api_torrent_action_forget(&self, idx: TorrentId) -> Result { - Err(ApiError::not_implemented("forgetting not implemented yet")) + self.session.delete(idx, false)?; + Ok(Default::default()) } fn api_torrent_action_delete(&self, idx: TorrentId) -> Result { - Err(ApiError::not_implemented( - "deleting torrent not implemented yet", - )) + self.session.delete(idx, true)?; + Ok(Default::default()) } pub async fn api_add_torrent( diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index e073a84..143e8b0 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -1,4 +1,6 @@ -use std::{borrow::Cow, io::Read, net::SocketAddr, path::PathBuf, time::Duration}; +use std::{ + borrow::Cow, collections::HashMap, io::Read, net::SocketAddr, path::PathBuf, time::Duration, +}; use anyhow::{bail, Context}; use buffers::ByteString; @@ -26,13 +28,15 @@ pub type TorrentId = usize; #[derive(Default)] pub struct SessionLocked { - torrents: Vec, + next_id: usize, + torrents: HashMap, } impl SessionLocked { fn add_torrent(&mut self, torrent: ManagedTorrentHandle) -> TorrentId { - let idx = self.torrents.len(); - self.torrents.push(torrent); + let idx = self.next_id; + self.torrents.insert(idx, torrent); + self.next_id += 1; idx } } @@ -205,7 +209,7 @@ impl Session { &self, callback: impl Fn(&mut dyn Iterator) -> R, ) -> R { - callback(&mut self.locked.read().torrents.iter().enumerate()) + callback(&mut self.locked.read().torrents.iter().map(|(id, t)| (*id, t))) } pub async fn add_torrent( @@ -404,13 +408,9 @@ impl Session { let (managed_torrent, id) = { let mut g = self.locked.write(); - if let Some((id, handle)) = g - .torrents - .iter() - .enumerate() - .find(|(_, t)| t.info_hash() == info_hash) + if let Some((id, handle)) = g.torrents.iter().find(|(_, t)| t.info_hash() == info_hash) { - return Ok(AddTorrentResponse::AlreadyManaged(id, handle.clone())); + return Ok(AddTorrentResponse::AlreadyManaged(*id, handle.clone())); } let next_id = g.torrents.len(); let managed_torrent = builder.build(error_span!("torrent", id = next_id))?; @@ -430,7 +430,25 @@ impl Session { } pub fn get(&self, id: TorrentId) -> Option { - self.locked.read().torrents.get(id).cloned() + self.locked.read().torrents.get(&id).cloned() + } + + pub fn delete(&self, id: TorrentId, delete_files: bool) -> anyhow::Result<()> { + let removed = self + .locked + .write() + .torrents + .remove(&id) + .with_context(|| format!("torrent with id {} did not exist", id))?; + + if let Some(live) = removed.live() { + let _ = live.pause()?; + } + + if delete_files { + bail!("torrent deleted, but deleting files not implemented") + } + Ok(()) } pub fn unpause(&self, handle: &ManagedTorrentHandle) -> anyhow::Result<()> { diff --git a/crates/librqbit/webui/vite.config.ts b/crates/librqbit/webui/vite.config.ts index 877950a..2317a04 100644 --- a/crates/librqbit/webui/vite.config.ts +++ b/crates/librqbit/webui/vite.config.ts @@ -1,5 +1,4 @@ import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ From 73e41ba7d5494c65cbda5d04133ad49edc3ad224 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 23:18:09 +0000 Subject: [PATCH 21/38] fix js delete torrent bug --- crates/librqbit/webui/src/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 57d2829..9e41ea7 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -40,7 +40,7 @@ const DeleteTorrentModal = ({ id, show, onHide }) => { const [error, setError] = useState(null); const [deleting, setDeleting] = useState(false); - const refreshCtx = useContext(RefreshTorrentStatsContext); + const ctx = useContext(AppContext); const close = () => { setDeleteFiles(false); @@ -55,7 +55,7 @@ const DeleteTorrentModal = ({ id, show, onHide }) => { const call = deleteFiles ? API.delete : API.forget; call(id).then(() => { - refreshCtx.refresh(); + ctx.refreshTorrents(); close(); }).catch((e) => { setError({ From 17b243921d3e127d5635f4bf8b2d8d020b16a82d Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 00:24:32 +0000 Subject: [PATCH 22/38] Continuing refactor --- .cargo/config | 5 +- Cargo.lock | 258 +++++++++++++++++- TODO.md | 22 +- crates/librqbit/src/http_api.rs | 8 +- crates/librqbit/src/http_api_error.rs | 1 + crates/librqbit/src/session.rs | 34 ++- crates/librqbit/src/spawn_utils.rs | 7 +- crates/librqbit/src/torrent_state/live/mod.rs | 12 +- crates/librqbit/src/torrent_state/mod.rs | 22 +- crates/rqbit/Cargo.toml | 4 +- crates/rqbit/src/main.rs | 86 ++++-- 11 files changed, 395 insertions(+), 64 deletions(-) diff --git a/.cargo/config b/.cargo/config index 0aca9d9..1b76467 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,5 @@ [target.arm-unknown-linux-gnueabihf] -rustflags = ["-l", "atomic"] \ No newline at end of file +rustflags = ["-l", "atomic"] + +[build] +rustflags = ["--cfg", "tokio_unstable"] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ff0e522..60c88b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,28 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.74" @@ -317,6 +339,43 @@ dependencies = [ "libc", ] +[[package]] +name = "console-api" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -342,6 +401,34 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -371,7 +458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.2", "lock_api", "once_cell", "parking_lot_core", @@ -451,6 +538,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -624,19 +721,38 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64", + "byteorder", + "flate2", + "nom", + "num-traits", +] + [[package]] name = "heck" version = "0.4.1" @@ -701,6 +817,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.27" @@ -739,6 +861,18 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -762,6 +896,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.1.0" @@ -769,7 +913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.2", ] [[package]] @@ -787,6 +931,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.0" @@ -860,7 +1013,7 @@ dependencies = [ "futures", "hex 0.4.3", "http", - "itertools", + "itertools 0.12.0", "librqbit-bencode", "librqbit-buffers", "librqbit-clone-to-owned", @@ -917,7 +1070,7 @@ version = "3.0.0" dependencies = [ "anyhow", "hex 0.4.3", - "itertools", + "itertools 0.12.0", "librqbit-bencode", "librqbit-buffers", "librqbit-clone-to-owned", @@ -935,7 +1088,7 @@ dependencies = [ "directories", "futures", "hex 0.4.3", - "indexmap", + "indexmap 2.1.0", "leaky-bucket", "librqbit-bencode", "librqbit-clone-to-owned", @@ -1023,6 +1176,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1061,6 +1220,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1279,7 +1448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.1.0", ] [[package]] @@ -1335,6 +1504,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.33" @@ -1507,6 +1708,7 @@ version = "3.3.0" dependencies = [ "anyhow", "clap", + "console-subscriber", "futures", "librqbit", "librqbit-dht", @@ -1888,9 +2090,20 @@ dependencies = [ "pin-project-lite", "socket2 0.5.5", "tokio-macros", + "tracing", "windows-sys", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "2.2.0" @@ -1948,6 +2161,33 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -1956,9 +2196,13 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "indexmap 1.9.3", "pin-project", "pin-project-lite", + "rand", + "slab", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", diff --git a/TODO.md b/TODO.md index d91f504..118a23c 100644 --- a/TODO.md +++ b/TODO.md @@ -5,23 +5,25 @@ - [x] tracing instead of logging. Debugging peers: RUST_LOG=[{peer=.*}]=debug test-log for tests - [x] reopen read only is bugged -- [ ] initializing/checking - - [ ] blocks the whole process. Need to break it up. On slower devices (rpi) just hangs for a good while - - [ ] checking torrents should be visible right away +- [x] initializing/checking + - [x] blocks the whole process. Need to break it up. On slower devices (rpi) just hangs for a good while + - [x] checking torrents should be visible right away - [ ] server persistence - [ ] it would be nice to restart the server and keep the state -- [ ] torrent actions - - [ ] pause/unpause - - [ ] remove including from disk +- [x] torrent actions + - [x] pause/unpause + - [x] remove including from disk - [ ] DHT - [ ] for torrents with a few seeds might be cool to re-query DHT once in a while - - [ ] it's sending many requests now way too fast, locks up Mac OS UI annoyingly + - [x] it's sending many requests now way too fast, locks up Mac OS UI annoyingly someday: - [ ] cancellation from the client-side for the lib (i.e. stop the torrent manager) refactor: -- [ ] where are peers stored -- [ ] http api pause/unpause etc -- [ ] when a live torrent fails writing to disk, it should transition to error state \ No newline at end of file +- [x] where are peers stored +- [x] http api pause/unpause etc +- [ ] when a live torrent fails writing to disk, it should transition to error state +- [ ] something is wrong when unpausing - can't finish. Recalculate needed/have from chunk tracker. +- [ ] silence this: WARN torrent{id=0}:external_peer_adder: librqbit::spawn_utils: finished with error: no longer live diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index f461384..0b112ba 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -447,12 +447,16 @@ impl ApiInternal { } fn api_torrent_action_forget(&self, idx: TorrentId) -> Result { - self.session.delete(idx, false)?; + self.session + .delete(idx, false) + .context("error forgetting torrent")?; Ok(Default::default()) } fn api_torrent_action_delete(&self, idx: TorrentId) -> Result { - self.session.delete(idx, true)?; + self.session + .delete(idx, true) + .context("error deleting torrent with files")?; Ok(Default::default()) } diff --git a/crates/librqbit/src/http_api_error.rs b/crates/librqbit/src/http_api_error.rs index 628eb34..46a04ae 100644 --- a/crates/librqbit/src/http_api_error.rs +++ b/crates/librqbit/src/http_api_error.rs @@ -19,6 +19,7 @@ impl ApiError { } } + #[allow(dead_code)] pub fn not_implemented(msg: &str) -> Self { Self { status: Some(StatusCode::INTERNAL_SERVER_ERROR), diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 143e8b0..6733eb9 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -19,7 +19,7 @@ use crate::{ dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult}, peer_connection::PeerConnectionOptions, spawn_utils::BlockingSpawner, - torrent_state::{ManagedTorrentBuilder, ManagedTorrentHandle}, + torrent_state::{ManagedTorrentBuilder, ManagedTorrentHandle, ManagedTorrentState}, }; pub const SUPPORTED_SCHEMES: [&str; 3] = ["http:", "https:", "magnet:"]; @@ -441,14 +441,34 @@ impl Session { .remove(&id) .with_context(|| format!("torrent with id {} did not exist", id))?; - if let Some(live) = removed.live() { - let _ = live.pause()?; - } + let paused = removed + .with_state_mut(|s| { + let paused = match s.take() { + ManagedTorrentState::Paused(p) => p, + ManagedTorrentState::Live(l) => l.pause()?, + _ => return Ok(None), + }; + Ok::<_, anyhow::Error>(Some(paused)) + }) + .context("error pausing torrent"); - if delete_files { - bail!("torrent deleted, but deleting files not implemented") + match (paused, delete_files) { + (Err(e), true) => Err(e).context("torrent deleted, but could not delete files"), + (Err(e), false) => { + warn!("could not delete torrent files: {:?}", e); + Ok(()) + } + (Ok(Some(paused)), true) => { + drop(paused.files); + for file in paused.filenames { + if let Err(e) = std::fs::remove_file(&file) { + warn!("could not delete file {:?}: {:?}", file, e); + } + } + Ok(()) + } + _ => Ok(()), } - Ok(()) } pub fn unpause(&self, handle: &ManagedTorrentHandle) -> anyhow::Result<()> { diff --git a/crates/librqbit/src/spawn_utils.rs b/crates/librqbit/src/spawn_utils.rs index 3b54967..957a837 100644 --- a/crates/librqbit/src/spawn_utils.rs +++ b/crates/librqbit/src/spawn_utils.rs @@ -1,6 +1,7 @@ -use tracing::{debug, trace, Instrument}; +use tracing::{debug, trace, warn, Instrument}; pub fn spawn( + name: &str, span: tracing::Span, fut: impl std::future::Future> + Send + 'static, ) -> tokio::task::JoinHandle<()> { @@ -11,12 +12,12 @@ pub fn spawn( debug!("finished"); } Err(e) => { - debug!("finished with error: {:#}", e) + warn!("finished with error: {:#}", e) } } } .instrument(span.or_current()); - tokio::spawn(fut) + tokio::task::Builder::new().name(name).spawn(fut).unwrap() } #[derive(Clone, Copy, Debug)] diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index faa2955..48171b5 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -226,12 +226,14 @@ impl TorrentStateLive { for tracker in state.meta.trackers.iter() { state.spawn( + "tracker_monitor", error_span!(parent: state.meta.span.clone(), "tracker_monitor", url = tracker.to_string()), state.clone().task_single_tracker_monitor(tracker.clone()), ); } state.spawn( + "speed_estimator_updater", error_span!(parent: state.meta.span.clone(), "speed_estimator_updater"), { let state = Arc::downgrade(&state); @@ -258,6 +260,7 @@ impl TorrentStateLive { ); state.spawn( + "peer_adder", error_span!(parent: state.meta.span.clone(), "peer_adder"), state.clone().task_peer_adder(peer_queue_rx), ); @@ -266,15 +269,17 @@ impl TorrentStateLive { fn spawn( &self, + name: &str, span: tracing::Span, fut: impl std::future::Future> + Send + 'static, ) { let mut cancel_rx = self.cancel_rx.clone(); - spawn(span, async move { + spawn(name, span, async move { tokio::select! { r = fut => r, _ = cancel_rx.changed() => { - bail!("canceled") + debug!("task canceled"); + Ok(()) } } }); @@ -429,6 +434,7 @@ impl TorrentStateLive { let permit = state.peer_semaphore.acquire().await?; permit.forget(); state.spawn( + "manage_peer", error_span!(parent: state.meta.span.clone(), "manage_peer", peer = addr.to_string()), state.clone().task_manage_peer(addr), ); @@ -568,6 +574,7 @@ impl TorrentStateLive { // We don't want to remember this task as there may be too many. self.spawn( + "transmit_haves", error_span!( parent: self.meta.span.clone(), "transmit_haves", @@ -830,6 +837,7 @@ impl PeerHandler { if let Some(dur) = backoff { self.state.clone().spawn( + "wait_for_peer", error_span!( parent: self.state.meta.span.clone(), "wait_for_peer", diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 793e8f3..502a2dd 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -3,6 +3,7 @@ pub mod live; pub mod paused; pub mod utils; +use std::collections::HashSet; use std::net::SocketAddr; use std::path::Path; use std::path::PathBuf; @@ -22,6 +23,7 @@ pub use live::*; use parking_lot::RwLock; use tokio_stream::StreamExt; +use tracing::debug; use tracing::error; use tracing::error_span; use url::Url; @@ -52,7 +54,7 @@ impl ManagedTorrentState { } } - fn take(&mut self) -> Self { + pub(crate) fn take(&mut self) -> Self { std::mem::replace(self, Self::None) } } @@ -74,7 +76,7 @@ pub struct ManagedTorrentInfo { pub info_hash: Id20, pub out_dir: PathBuf, pub spawner: BlockingSpawner, - pub trackers: Vec, + pub trackers: HashSet, pub peer_id: Id20, pub lengths: Lengths, pub span: tracing::Span, @@ -108,6 +110,10 @@ impl ManagedTorrent { f(&self.locked.read().state) } + pub(crate) fn with_state_mut(&self, f: impl FnOnce(&mut ManagedTorrentState) -> R) -> R { + f(&mut self.locked.write().state) + } + pub fn with_chunk_tracker(&self, f: impl FnOnce(&ChunkTracker) -> R) -> anyhow::Result { let g = self.locked.read(); match &g.state { @@ -167,14 +173,23 @@ impl ManagedTorrent { let init = init.clone(); let t = self.clone(); spawn( + "initialize_and_start", error_span!(parent: span.clone(), "initialize_and_start"), async move { match init.check().await { Ok(paused) => { + let mut g = t.locked.write(); + if let ManagedTorrentState::Initializing(_) = &g.state { + } else { + debug!("no need to start torrent anymore, as it switched state from initilizing"); + return Ok(()); + } + let live = TorrentStateLive::new(paused); - t.locked.write().state = ManagedTorrentState::Live(live.clone()); + g.state = ManagedTorrentState::Live(live.clone()); spawn( + "external_peer_adder", error_span!(parent: span.clone(), "external_peer_adder"), peer_adder(Arc::downgrade(&live)), ); @@ -196,6 +211,7 @@ impl ManagedTorrent { let live = TorrentStateLive::new(paused); g.state = ManagedTorrentState::Live(live.clone()); spawn( + "external_peer_adder", error_span!(parent: span.clone(), "external_peer_adder"), peer_adder(Arc::downgrade(&live)), ); diff --git a/crates/rqbit/Cargo.toml b/crates/rqbit/Cargo.toml index 18efde5..4e73d12 100644 --- a/crates/rqbit/Cargo.toml +++ b/crates/rqbit/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" [features] default = ["sha1-system", "default-tls", "webui"] +tokio-console = ["console-subscriber"] webui = ["librqbit/webui"] timed_existence = ["librqbit/timed_existence"] sha1-system = ["librqbit/sha1-system"] @@ -24,7 +25,8 @@ rust-tls = ["librqbit/rust-tls"] [dependencies] librqbit = {path="../librqbit", default-features=false, version = "3.3.0"} dht = {path="../dht", package="librqbit-dht", version="3.1.0"} -tokio = {version = "1", features = ["macros", "rt-multi-thread"]} +tokio = {version = "1", features = ["macros", "rt-multi-thread", "tracing"]} +console-subscriber = {version = "0.2", optional = true} anyhow = "1" clap = {version = "4", features = ["derive", "deprecated"]} tracing = "0.1" diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index be85289..87becb3 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -14,7 +14,7 @@ use librqbit::{ torrent_state::ManagedTorrentState, }; use size_format::SizeFormatterBinary as SF; -use tracing::{error, info, span, warn, Level}; +use tracing::{error, error_span, info, trace_span, warn}; #[derive(Debug, Clone, Copy, ValueEnum)] enum LogLevel { @@ -135,30 +135,58 @@ enum SubCommand { } fn init_logging(opts: &Opts) { - if std::env::var_os("RUST_LOG").is_none() { - match opts.log_level.as_ref() { - Some(level) => { - let level_str = match level { - LogLevel::Trace => "trace", - LogLevel::Debug => "debug", - LogLevel::Info => "info", - LogLevel::Warn => "warn", - LogLevel::Error => "error", - }; - std::env::set_var("RUST_LOG", level_str); - } - None => { - std::env::set_var("RUST_LOG", "info"); - } - }; - } + let default_rust_log = match opts.log_level.as_ref() { + Some(level) => match level { + LogLevel::Trace => "trace", + LogLevel::Debug => "debug", + LogLevel::Info => "info", + LogLevel::Warn => "warn", + LogLevel::Error => "error", + }, + None => "info", + }; + let stderr_filter = match std::env::var("RUST_LOG").ok() { + Some(rust_log) => EnvFilter::builder() + .parse(&rust_log) + .expect("can't parse RUST_LOG"), + None => EnvFilter::builder() + .parse(default_rust_log) + .expect("can't parse default_rust_log"), + }; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; - tracing_subscriber::registry() - .with(fmt::layer()) - .with(EnvFilter::from_default_env()) - .init(); + #[cfg(feature = "tokio-console")] + { + let (console_layer, server) = console_subscriber::Builder::default() + .with_default_env() + .build(); + + tracing_subscriber::registry() + .with(fmt::layer().with_filter(stderr_filter)) + .with(console_layer) + .init(); + + spawn( + "console_subscriber server", + error_span!("console_subscriber server"), + async move { + server + .serve() + .await + .map_err(|e| anyhow::anyhow!("{:#?}", e)) + .context("error running console subscriber server") + }, + ); + } + + #[cfg(not(feature = "tokio-console"))] + { + tracing_subscriber::registry() + .with(fmt::layer()) + .with(stderr_filter) + .init(); + } } fn _start_deadlock_detector_thread() { @@ -188,9 +216,6 @@ fn _start_deadlock_detector_thread() { fn main() -> anyhow::Result<()> { let opts = Opts::parse(); - init_logging(&opts); - // start_deadlock_detector_thread(); - let (mut rt_builder, spawner) = match opts.single_thread_runtime { true => ( tokio::runtime::Builder::new_current_thread(), @@ -223,6 +248,8 @@ fn main() -> anyhow::Result<()> { } async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> { + init_logging(&opts); + let sopts = SessionOptions { disable_dht: opts.disable_dht, disable_dht_persistence: opts.disable_dht_persistence, @@ -300,7 +327,8 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> .context("error initializing rqbit session")?, ); spawn( - span!(Level::TRACE, "stats_printer"), + "stats_printer", + trace_span!("stats_printer"), stats_printer(session.clone()), ); let http_api = HttpApi::new(session); @@ -379,13 +407,15 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> .context("error initializing rqbit session")?, ); spawn( - span!(Level::TRACE, "stats_printer"), + "stats_printer", + trace_span!("stats_printer"), stats_printer(session.clone()), ); let http_api = HttpApi::new(session.clone()); let http_api_listen_addr = opts.http_api_listen_addr; spawn( - span!(Level::ERROR, "http_api"), + "http_api", + error_span!("http_api"), http_api.clone().make_http_api_and_run(http_api_listen_addr), ); From fa97dedb989391fa5f9d8f97748c3c09477ec3b3 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 00:54:21 +0000 Subject: [PATCH 23/38] Seems alright now --- crates/librqbit/src/chunk_tracker.rs | 10 +++++ crates/librqbit/src/torrent_state/live/mod.rs | 37 ++++++++++--------- crates/librqbit/webui/dist/app.js | 20 +++++----- crates/librqbit/webui/dist/index.html | 2 + crates/librqbit/webui/src/index.tsx | 12 ++++-- 5 files changed, 50 insertions(+), 31 deletions(-) diff --git a/crates/librqbit/src/chunk_tracker.rs b/crates/librqbit/src/chunk_tracker.rs index ecaeab5..008e13e 100644 --- a/crates/librqbit/src/chunk_tracker.rs +++ b/crates/librqbit/src/chunk_tracker.rs @@ -87,6 +87,16 @@ impl ChunkTracker { self.needed_pieces.set(index.get() as usize, false) } + pub fn calc_have_bytes(&self) -> u64 { + self.have + .iter_ones() + .filter_map(|piece_id| { + let piece_id = self.lengths.validate_piece_index(piece_id as u32)?; + Some(self.lengths.piece_length(piece_id) as u64) + }) + .sum() + } + pub fn iter_needed_pieces(&self) -> impl Iterator + '_ { self.priority_piece_ids .iter() diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 48171b5..68f035d 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -167,9 +167,7 @@ pub struct TorrentStateLive { files: Vec>>, filenames: Vec, - // TODO: why the hell do we need these here, remove it. - needed_bytes: u64, - have_plus_needed_bytes: u64, + initially_needed_bytes: u64, stats: AtomicStats, lengths: Lengths, @@ -213,8 +211,7 @@ impl TorrentStateLive { have_bytes: AtomicU64::new(have_bytes), ..Default::default() }, - needed_bytes, - have_plus_needed_bytes: needed_bytes + have_bytes, + initially_needed_bytes: needed_bytes, lengths, peer_semaphore: Semaphore::new(128), peer_queue_tx, @@ -454,7 +451,7 @@ impl TorrentStateLive { FileOps::new(&self.meta.info, &self.files, &self.lengths) } pub fn initially_needed(&self) -> u64 { - self.needed_bytes + self.initially_needed_bytes } pub(crate) fn lock_read( @@ -518,7 +515,7 @@ impl TorrentStateLive { .load(Ordering::Acquire) } - pub fn get_have_bytes(&self) -> u64 { + pub fn get_approx_have_bytes(&self) -> u64 { self.stats.have_bytes.load(Ordering::Relaxed) } @@ -527,7 +524,7 @@ impl TorrentStateLive { } pub fn get_left_to_download_bytes(&self) -> u64 { - self.needed_bytes - self.get_downloaded_bytes() + self.initially_needed_bytes - self.get_downloaded_bytes() } fn maybe_transmit_haves(&self, index: ValidPieceIndex) { @@ -601,15 +598,15 @@ impl TorrentStateLive { pub fn stats_snapshot(&self) -> StatsSnapshot { use Ordering::*; let downloaded_bytes = self.stats.downloaded_and_checked_bytes.load(Relaxed); - let remaining = self.needed_bytes - downloaded_bytes; + let remaining = self.initially_needed_bytes - downloaded_bytes; StatsSnapshot { have_bytes: self.stats.have_bytes.load(Relaxed), downloaded_and_checked_bytes: downloaded_bytes, downloaded_and_checked_pieces: self.stats.downloaded_and_checked_pieces.load(Relaxed), fetched_bytes: self.stats.fetched_bytes.load(Relaxed), uploaded_bytes: self.stats.uploaded_bytes.load(Relaxed), - total_bytes: self.have_plus_needed_bytes, - initially_needed_bytes: self.needed_bytes, + total_bytes: self.lengths.total_length(), + initially_needed_bytes: self.initially_needed_bytes, remaining_bytes: remaining, total_piece_download_ms: self.stats.total_piece_download_ms.load(Relaxed), peer_stats: self.peers.stats(), @@ -653,16 +650,22 @@ impl TorrentStateLive { let filenames = self.filenames.clone(); + let mut chunk_tracker = g + .chunks + .take() + .context("bug: pausing already paused torrent")?; + for piece_id in g.inflight_pieces.keys().copied() { + chunk_tracker.mark_piece_broken(piece_id); + } + let have_bytes = chunk_tracker.calc_have_bytes(); + // g.chunks; Ok(TorrentStatePaused { info: self.meta.clone(), files, filenames, - chunk_tracker: g - .chunks - .take() - .context("bug: pausing already paused torrent")?, - have_bytes: self.get_have_bytes(), + chunk_tracker, + have_bytes, }) } } @@ -765,7 +768,7 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler { } fn get_have_bytes(&self) -> u64 { - self.state.get_have_bytes() + self.state.get_approx_have_bytes() } } diff --git a/crates/librqbit/webui/dist/app.js b/crates/librqbit/webui/dist/app.js index bb1abab..06595b2 100644 --- a/crates/librqbit/webui/dist/app.js +++ b/crates/librqbit/webui/dist/app.js @@ -1,4 +1,4 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function Vl(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var va={exports:{}},Ql={},ya={exports:{}},F={};/** +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function Gl(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var wa={exports:{}},Yl={},Sa={exports:{}},F={};/** * @license React * react.production.min.js * @@ -6,7 +6,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Or=Symbol.for("react.element"),Dd=Symbol.for("react.portal"),Id=Symbol.for("react.fragment"),Ad=Symbol.for("react.strict_mode"),Ud=Symbol.for("react.profiler"),Bd=Symbol.for("react.provider"),Hd=Symbol.for("react.context"),Wd=Symbol.for("react.forward_ref"),Vd=Symbol.for("react.suspense"),Qd=Symbol.for("react.memo"),Kd=Symbol.for("react.lazy"),Yu=Symbol.iterator;function Gd(e){return e===null||typeof e!="object"?null:(e=Yu&&e[Yu]||e["@@iterator"],typeof e=="function"?e:null)}var ga={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},wa=Object.assign,Sa={};function zn(e,t,n){this.props=e,this.context=t,this.refs=Sa,this.updater=n||ga}zn.prototype.isReactComponent={};zn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};zn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function ka(){}ka.prototype=zn.prototype;function Ki(e,t,n){this.props=e,this.context=t,this.refs=Sa,this.updater=n||ga}var Gi=Ki.prototype=new ka;Gi.constructor=Ki;wa(Gi,zn.prototype);Gi.isPureReactComponent=!0;var Xu=Array.isArray,Ea=Object.prototype.hasOwnProperty,Yi={current:null},xa={key:!0,ref:!0,__self:!0,__source:!0};function Ca(e,t,n){var r,l={},o=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(o=""+t.key),t)Ea.call(t,r)&&!xa.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,A=x[D];if(0>>1;Dl(qe,O))jel(dt,qe)?(x[D]=dt,x[je]=O,D=je):(x[D]=qe,x[Re]=O,D=Re);else if(jel(dt,O))x[D]=dt,x[je]=O,D=je;else break e}}return j}function l(x,j){var O=x.sortIndex-j.sortIndex;return O!==0?O:x.id-j.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],a=[],d=1,h=null,p=3,g=!1,S=!1,E=!1,L=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function m(x){for(var j=n(a);j!==null;){if(j.callback===null)r(a);else if(j.startTime<=x)r(a),j.sortIndex=j.expirationTime,t(s,j);else break;j=n(a)}}function w(x){if(E=!1,m(x),!S)if(n(s)!==null)S=!0,_e(C);else{var j=n(a);j!==null&&Ke(w,j.startTime-x)}}function C(x,j){S=!1,E&&(E=!1,f(R),R=-1),g=!0;var O=p;try{for(m(j),h=n(s);h!==null&&(!(h.expirationTime>j)||x&&!ie());){var D=h.callback;if(typeof D=="function"){h.callback=null,p=h.priorityLevel;var A=D(h.expirationTime<=j);j=e.unstable_now(),typeof A=="function"?h.callback=A:h===n(s)&&r(s),m(j)}else r(s);h=n(s)}if(h!==null)var fe=!0;else{var Re=n(a);Re!==null&&Ke(w,Re.startTime-j),fe=!1}return fe}finally{h=null,p=O,g=!1}}var N=!1,T=null,R=-1,U=5,P=-1;function ie(){return!(e.unstable_now()-Px||125D?(x.sortIndex=O,t(a,x),n(s)===null&&x===n(a)&&(E?(f(R),R=-1):E=!0,Ke(w,O-D))):(x.sortIndex=A,t(s,x),S||g||(S=!0,_e(C))),x},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(x){var j=p;return function(){var O=p;p=j;try{return x.apply(this,arguments)}finally{p=O}}}})(Ra);_a.exports=Ra;var lp=_a.exports;/** + */(function(e){function t(x,R){var O=x.length;x.push(R);e:for(;0>>1,A=x[D];if(0>>1;Dl(qe,O))Lel(mt,qe)?(x[D]=mt,x[Le]=O,D=Le):(x[D]=qe,x[je]=O,D=je);else if(Lel(mt,O))x[D]=mt,x[Le]=O,D=Le;else break e}}return R}function l(x,R){var O=x.sortIndex-R.sortIndex;return O!==0?O:x.id-R.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],a=[],d=1,m=null,p=3,g=!1,w=!1,k=!1,L=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function h(x){for(var R=n(a);R!==null;){if(R.callback===null)r(a);else if(R.startTime<=x)r(a),R.sortIndex=R.expirationTime,t(s,R);else break;R=n(a)}}function S(x){if(k=!1,h(x),!w)if(n(s)!==null)w=!0,_e(C);else{var R=n(a);R!==null&&Ke(S,R.startTime-x)}}function C(x,R){w=!1,k&&(k=!1,f(j),j=-1),g=!0;var O=p;try{for(h(R),m=n(s);m!==null&&(!(m.expirationTime>R)||x&&!ie());){var D=m.callback;if(typeof D=="function"){m.callback=null,p=m.priorityLevel;var A=D(m.expirationTime<=R);R=e.unstable_now(),typeof A=="function"?m.callback=A:m===n(s)&&r(s),h(R)}else r(s);m=n(s)}if(m!==null)var fe=!0;else{var je=n(a);je!==null&&Ke(S,je.startTime-R),fe=!1}return fe}finally{m=null,p=O,g=!1}}var N=!1,T=null,j=-1,U=5,P=-1;function ie(){return!(e.unstable_now()-Px||125D?(x.sortIndex=O,t(a,x),n(s)===null&&x===n(a)&&(k?(f(j),j=-1):k=!0,Ke(S,O-D))):(x.sortIndex=A,t(s,x),w||g||(w=!0,_e(C))),x},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(x){var R=p;return function(){var O=p;p=R;try{return x.apply(this,arguments)}finally{p=O}}}})(Oa);Ra.exports=Oa;var ap=Ra.exports;/** * @license React * react-dom.production.min.js * @@ -30,15 +30,15 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var ja=v,Ce=lp;function k(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ko=Object.prototype.hasOwnProperty,op=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Ju={},qu={};function ip(e){return Ko.call(qu,e)?!0:Ko.call(Ju,e)?!1:op.test(e)?qu[e]=!0:(Ju[e]=!0,!1)}function up(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function sp(e,t,n,r){if(t===null||typeof t>"u"||up(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function he(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var oe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){oe[e]=new he(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];oe[t]=new he(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){oe[e]=new he(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){oe[e]=new he(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){oe[e]=new he(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){oe[e]=new he(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){oe[e]=new he(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){oe[e]=new he(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){oe[e]=new he(e,5,!1,e.toLowerCase(),null,!1,!1)});var Zi=/[\-:]([a-z])/g;function Ji(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Zi,Ji);oe[t]=new he(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Zi,Ji);oe[t]=new he(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Zi,Ji);oe[t]=new he(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!1,!1)});oe.xlinkHref=new he("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!0,!0)});function qi(e,t,n,r){var l=oe.hasOwnProperty(t)?oe[t]:null;(l!==null?l.type!==0:r||!(2"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Yo=Object.prototype.hasOwnProperty,cp=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,es={},ts={};function fp(e){return Yo.call(ts,e)?!0:Yo.call(es,e)?!1:cp.test(e)?ts[e]=!0:(es[e]=!0,!1)}function dp(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function pp(e,t,n,r){if(t===null||typeof t>"u"||dp(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function he(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var oe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){oe[e]=new he(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];oe[t]=new he(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){oe[e]=new he(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){oe[e]=new he(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){oe[e]=new he(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){oe[e]=new he(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){oe[e]=new he(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){oe[e]=new he(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){oe[e]=new he(e,5,!1,e.toLowerCase(),null,!1,!1)});var qi=/[\-:]([a-z])/g;function bi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!1,!1)});oe.xlinkHref=new he("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!0,!0)});function eu(e,t,n,r){var l=oe.hasOwnProperty(t)?oe[t]:null;(l!==null?l.type!==0:r||!(2u||l[i]!==o[u]){var s=` -`+l[i].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{ho=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?qn(e):""}function ap(e){switch(e.tag){case 5:return qn(e.type);case 16:return qn("Lazy");case 13:return qn("Suspense");case 19:return qn("SuspenseList");case 0:case 2:case 15:return e=vo(e.type,!1),e;case 11:return e=vo(e.type.render,!1),e;case 1:return e=vo(e.type,!0),e;default:return""}}function Zo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case cn:return"Fragment";case an:return"Portal";case Go:return"Profiler";case bi:return"StrictMode";case Yo:return"Suspense";case Xo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Pa:return(e.displayName||"Context")+".Consumer";case Oa:return(e._context.displayName||"Context")+".Provider";case eu:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case tu:return t=e.displayName||null,t!==null?t:Zo(e.type)||"Memo";case mt:t=e._payload,e=e._init;try{return Zo(e(t))}catch{}}return null}function cp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Zo(t);case 8:return t===bi?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function jt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ma(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function fp(e){var t=Ma(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Ur(e){e._valueTracker||(e._valueTracker=fp(e))}function za(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Ma(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function vl(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Jo(e,t){var n=t.checked;return X({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function es(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=jt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function $a(e,t){t=t.checked,t!=null&&qi(e,"checked",t,!1)}function qo(e,t){$a(e,t);var n=jt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?bo(e,t.type,n):t.hasOwnProperty("defaultValue")&&bo(e,t.type,jt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ts(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function bo(e,t,n){(t!=="number"||vl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var bn=Array.isArray;function En(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Br.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function pr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var rr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},dp=["Webkit","ms","Moz","O"];Object.keys(rr).forEach(function(e){dp.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),rr[t]=rr[e]})});function Ua(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||rr.hasOwnProperty(e)&&rr[e]?(""+t).trim():t+"px"}function Ba(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Ua(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var pp=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ni(e,t){if(t){if(pp[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(k(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(k(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(k(61))}if(t.style!=null&&typeof t.style!="object")throw Error(k(62))}}function ri(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var li=null;function nu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var oi=null,xn=null,Cn=null;function ls(e){if(e=Mr(e)){if(typeof oi!="function")throw Error(k(280));var t=e.stateNode;t&&(t=Zl(t),oi(e.stateNode,e.type,t))}}function Ha(e){xn?Cn?Cn.push(e):Cn=[e]:xn=e}function Wa(){if(xn){var e=xn,t=Cn;if(Cn=xn=null,ls(e),t)for(e=0;e>>=0,e===0?32:31-(Cp(e)/Np|0)|0}var Hr=64,Wr=4194304;function er(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Sl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=er(u):(o&=i,o!==0&&(r=er(o)))}else i=n&~l,i!==0?r=er(i):o!==0&&(r=er(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Pr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Be(t),e[t]=n}function jp(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=or),ps=String.fromCharCode(32),ms=!1;function ac(e,t){switch(e){case"keyup":return rm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function cc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var fn=!1;function om(e,t){switch(e){case"compositionend":return cc(t);case"keypress":return t.which!==32?null:(ms=!0,ps);case"textInput":return e=t.data,e===ps&&ms?null:e;default:return null}}function im(e,t){if(fn)return e==="compositionend"||!cu&&ac(e,t)?(e=uc(),ul=uu=wt=null,fn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=gs(n)}}function mc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?mc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function hc(){for(var e=window,t=vl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=vl(e.document)}return t}function fu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function hm(e){var t=hc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&mc(n.ownerDocument.documentElement,n)){if(r!==null&&fu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=ws(n,o);var i=ws(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,dn=null,fi=null,ur=null,di=!1;function Ss(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;di||dn==null||dn!==vl(r)||(r=dn,"selectionStart"in r&&fu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),ur&&wr(ur,r)||(ur=r,r=xl(fi,"onSelect"),0hn||(e.current=gi[hn],gi[hn]=null,hn--)}function B(e,t){hn++,gi[hn]=e.current,e.current=t}var Lt={},ce=Pt(Lt),ge=Pt(!1),Gt=Lt;function jn(e,t){var n=e.type.contextTypes;if(!n)return Lt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function we(e){return e=e.childContextTypes,e!=null}function Nl(){V(ge),V(ce)}function _s(e,t,n){if(ce.current!==Lt)throw Error(k(168));B(ce,t),B(ge,n)}function Cc(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(k(108,cp(e)||"Unknown",l));return X({},n,r)}function Tl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Lt,Gt=ce.current,B(ce,e),B(ge,ge.current),!0}function Rs(e,t,n){var r=e.stateNode;if(!r)throw Error(k(169));n?(e=Cc(e,t,Gt),r.__reactInternalMemoizedMergedChildContext=e,V(ge),V(ce),B(ce,e)):V(ge),B(ge,n)}var et=null,Jl=!1,Lo=!1;function Nc(e){et===null?et=[e]:et.push(e)}function _m(e){Jl=!0,Nc(e)}function Ft(){if(!Lo&&et!==null){Lo=!0;var e=0,t=I;try{var n=et;for(I=1;e>=i,l-=i,tt=1<<32-Be(t)+l|n<R?(U=T,T=null):U=T.sibling;var P=p(f,T,m[R],w);if(P===null){T===null&&(T=U);break}e&&T&&P.alternate===null&&t(f,T),c=o(P,c,R),N===null?C=P:N.sibling=P,N=P,T=U}if(R===m.length)return n(f,T),Q&&$t(f,R),C;if(T===null){for(;RR?(U=T,T=null):U=T.sibling;var ie=p(f,T,P.value,w);if(ie===null){T===null&&(T=U);break}e&&T&&ie.alternate===null&&t(f,T),c=o(ie,c,R),N===null?C=ie:N.sibling=ie,N=ie,T=U}if(P.done)return n(f,T),Q&&$t(f,R),C;if(T===null){for(;!P.done;R++,P=m.next())P=h(f,P.value,w),P!==null&&(c=o(P,c,R),N===null?C=P:N.sibling=P,N=P);return Q&&$t(f,R),C}for(T=r(f,T);!P.done;R++,P=m.next())P=g(T,f,R,P.value,w),P!==null&&(e&&P.alternate!==null&&T.delete(P.key===null?R:P.key),c=o(P,c,R),N===null?C=P:N.sibling=P,N=P);return e&&T.forEach(function(Ve){return t(f,Ve)}),Q&&$t(f,R),C}function L(f,c,m,w){if(typeof m=="object"&&m!==null&&m.type===cn&&m.key===null&&(m=m.props.children),typeof m=="object"&&m!==null){switch(m.$$typeof){case Ar:e:{for(var C=m.key,N=c;N!==null;){if(N.key===C){if(C=m.type,C===cn){if(N.tag===7){n(f,N.sibling),c=l(N,m.props.children),c.return=f,f=c;break e}}else if(N.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===mt&&zs(C)===N.type){n(f,N.sibling),c=l(N,m.props),c.ref=Yn(f,N,m),c.return=f,f=c;break e}n(f,N);break}else t(f,N);N=N.sibling}m.type===cn?(c=Qt(m.props.children,f.mode,w,m.key),c.return=f,f=c):(w=hl(m.type,m.key,m.props,null,f.mode,w),w.ref=Yn(f,c,m),w.return=f,f=w)}return i(f);case an:e:{for(N=m.key;c!==null;){if(c.key===N)if(c.tag===4&&c.stateNode.containerInfo===m.containerInfo&&c.stateNode.implementation===m.implementation){n(f,c.sibling),c=l(c,m.children||[]),c.return=f,f=c;break e}else{n(f,c);break}else t(f,c);c=c.sibling}c=Io(m,f.mode,w),c.return=f,f=c}return i(f);case mt:return N=m._init,L(f,c,N(m._payload),w)}if(bn(m))return S(f,c,m,w);if(Wn(m))return E(f,c,m,w);Zr(f,m)}return typeof m=="string"&&m!==""||typeof m=="number"?(m=""+m,c!==null&&c.tag===6?(n(f,c.sibling),c=l(c,m),c.return=f,f=c):(n(f,c),c=Do(m,f.mode,w),c.return=f,f=c),i(f)):n(f,c)}return L}var On=Fc(!0),Mc=Fc(!1),zr={},Je=Pt(zr),xr=Pt(zr),Cr=Pt(zr);function Wt(e){if(e===zr)throw Error(k(174));return e}function Su(e,t){switch(B(Cr,t),B(xr,e),B(Je,zr),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:ti(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=ti(t,e)}V(Je),B(Je,t)}function Pn(){V(Je),V(xr),V(Cr)}function zc(e){Wt(Cr.current);var t=Wt(Je.current),n=ti(t,e.type);t!==n&&(B(xr,e),B(Je,n))}function ku(e){xr.current===e&&(V(Je),V(xr))}var G=Pt(0);function Pl(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var Oo=[];function Eu(){for(var e=0;en?n:4,e(!0);var r=Po.transition;Po.transition={};try{e(!1),t()}finally{I=n,Po.transition=r}}function Jc(){return $e().memoizedState}function Om(e,t,n){var r=_t(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},qc(e))bc(t,n);else if(n=jc(e,t,n,r),n!==null){var l=pe();He(n,e,r,l),ef(n,t,r)}}function Pm(e,t,n){var r=_t(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(qc(e))bc(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,We(u,i)){var s=t.interleaved;s===null?(l.next=l,gu(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=jc(e,t,l,r),n!==null&&(l=pe(),He(n,e,r,l),ef(n,t,r))}}function qc(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function bc(e,t){sr=Fl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ef(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,lu(e,n)}}var Ml={readContext:ze,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},Fm={readContext:ze,useCallback:function(e,t){return Ye().memoizedState=[e,t===void 0?null:t],e},useContext:ze,useEffect:Ds,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,fl(4194308,4,Kc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return fl(4194308,4,e,t)},useInsertionEffect:function(e,t){return fl(4,2,e,t)},useMemo:function(e,t){var n=Ye();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ye();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Om.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ye();return e={current:e},t.memoizedState=e},useState:$s,useDebugValue:_u,useDeferredValue:function(e){return Ye().memoizedState=e},useTransition:function(){var e=$s(!1),t=e[0];return e=Lm.bind(null,e[1]),Ye().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,l=Ye();if(Q){if(n===void 0)throw Error(k(407));n=n()}else{if(n=t(),ne===null)throw Error(k(349));Xt&30||Ic(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Ds(Uc.bind(null,r,o,e),[e]),r.flags|=2048,_r(9,Ac.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ye(),t=ne.identifierPrefix;if(Q){var n=nt,r=tt;n=(r&~(1<<32-Be(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Nr++,0")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{yo=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?rr(e):""}function mp(e){switch(e.tag){case 5:return rr(e.type);case 16:return rr("Lazy");case 13:return rr("Suspense");case 19:return rr("SuspenseList");case 0:case 2:case 15:return e=go(e.type,!1),e;case 11:return e=go(e.type.render,!1),e;case 1:return e=go(e.type,!0),e;default:return""}}function qo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case pn:return"Fragment";case dn:return"Portal";case Xo:return"Profiler";case tu:return"StrictMode";case Zo:return"Suspense";case Jo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case za:return(e.displayName||"Context")+".Consumer";case Ma:return(e._context.displayName||"Context")+".Provider";case nu:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ru:return t=e.displayName||null,t!==null?t:qo(e.type)||"Memo";case gt:t=e._payload,e=e._init;try{return qo(e(t))}catch{}}return null}function hp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return qo(t);case 8:return t===tu?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Ft(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Da(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function vp(e){var t=Da(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Qr(e){e._valueTracker||(e._valueTracker=vp(e))}function Ia(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Da(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Sl(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function bo(e,t){var n=t.checked;return X({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function rs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Ft(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Aa(e,t){t=t.checked,t!=null&&eu(e,"checked",t,!1)}function ei(e,t){Aa(e,t);var n=Ft(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ti(e,t.type,n):t.hasOwnProperty("defaultValue")&&ti(e,t.type,Ft(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ls(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ti(e,t,n){(t!=="number"||Sl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var lr=Array.isArray;function Nn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Kr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function gr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var sr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},yp=["Webkit","ms","Moz","O"];Object.keys(sr).forEach(function(e){yp.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),sr[t]=sr[e]})});function Wa(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||sr.hasOwnProperty(e)&&sr[e]?(""+t).trim():t+"px"}function Va(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Wa(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var gp=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function li(e,t){if(t){if(gp[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(E(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(E(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(E(61))}if(t.style!=null&&typeof t.style!="object")throw Error(E(62))}}function oi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ii=null;function lu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ui=null,Tn=null,_n=null;function us(e){if(e=Ar(e)){if(typeof ui!="function")throw Error(E(280));var t=e.stateNode;t&&(t=bl(t),ui(e.stateNode,e.type,t))}}function Qa(e){Tn?_n?_n.push(e):_n=[e]:Tn=e}function Ka(){if(Tn){var e=Tn,t=_n;if(_n=Tn=null,us(e),t)for(e=0;e>>=0,e===0?32:31-(Lp(e)/Rp|0)|0}var Gr=64,Yr=4194304;function or(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Cl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=or(u):(o&=i,o!==0&&(r=or(o)))}else i=n&~l,i!==0?r=or(i):o!==0&&(r=or(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Dr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Be(t),e[t]=n}function Mp(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=cr),vs=String.fromCharCode(32),ys=!1;function dc(e,t){switch(e){case"keyup":return sm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function pc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var mn=!1;function cm(e,t){switch(e){case"compositionend":return pc(t);case"keypress":return t.which!==32?null:(ys=!0,vs);case"textInput":return e=t.data,e===vs&&ys?null:e;default:return null}}function fm(e,t){if(mn)return e==="compositionend"||!du&&dc(e,t)?(e=cc(),fl=au=xt=null,mn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ks(n)}}function yc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?yc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function gc(){for(var e=window,t=Sl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Sl(e.document)}return t}function pu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Sm(e){var t=gc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&yc(n.ownerDocument.documentElement,n)){if(r!==null&&pu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Es(n,o);var i=Es(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,hn=null,pi=null,dr=null,mi=!1;function xs(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;mi||hn==null||hn!==Sl(r)||(r=hn,"selectionStart"in r&&pu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),dr&&Cr(dr,r)||(dr=r,r=_l(pi,"onSelect"),0gn||(e.current=Si[gn],Si[gn]=null,gn--)}function B(e,t){gn++,Si[gn]=e.current,e.current=t}var Mt={},ce=$t(Mt),ge=$t(!1),Zt=Mt;function Fn(e,t){var n=e.type.contextTypes;if(!n)return Mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function we(e){return e=e.childContextTypes,e!=null}function Ll(){V(ge),V(ce)}function Rs(e,t,n){if(ce.current!==Mt)throw Error(E(168));B(ce,t),B(ge,n)}function _c(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(E(108,hp(e)||"Unknown",l));return X({},n,r)}function Rl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Mt,Zt=ce.current,B(ce,e),B(ge,ge.current),!0}function Os(e,t,n){var r=e.stateNode;if(!r)throw Error(E(169));n?(e=_c(e,t,Zt),r.__reactInternalMemoizedMergedChildContext=e,V(ge),V(ce),B(ce,e)):V(ge),B(ge,n)}var et=null,eo=!1,Po=!1;function jc(e){et===null?et=[e]:et.push(e)}function Pm(e){eo=!0,jc(e)}function Dt(){if(!Po&&et!==null){Po=!0;var e=0,t=I;try{var n=et;for(I=1;e>=i,l-=i,nt=1<<32-Be(t)+l|n<j?(U=T,T=null):U=T.sibling;var P=p(f,T,h[j],S);if(P===null){T===null&&(T=U);break}e&&T&&P.alternate===null&&t(f,T),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P,T=U}if(j===h.length)return n(f,T),Q&&At(f,j),C;if(T===null){for(;jj?(U=T,T=null):U=T.sibling;var ie=p(f,T,P.value,S);if(ie===null){T===null&&(T=U);break}e&&T&&ie.alternate===null&&t(f,T),c=o(ie,c,j),N===null?C=ie:N.sibling=ie,N=ie,T=U}if(P.done)return n(f,T),Q&&At(f,j),C;if(T===null){for(;!P.done;j++,P=h.next())P=m(f,P.value,S),P!==null&&(c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return Q&&At(f,j),C}for(T=r(f,T);!P.done;j++,P=h.next())P=g(T,f,j,P.value,S),P!==null&&(e&&P.alternate!==null&&T.delete(P.key===null?j:P.key),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return e&&T.forEach(function(Ve){return t(f,Ve)}),Q&&At(f,j),C}function L(f,c,h,S){if(typeof h=="object"&&h!==null&&h.type===pn&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Vr:e:{for(var C=h.key,N=c;N!==null;){if(N.key===C){if(C=h.type,C===pn){if(N.tag===7){n(f,N.sibling),c=l(N,h.props.children),c.return=f,f=c;break e}}else if(N.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===gt&&Is(C)===N.type){n(f,N.sibling),c=l(N,h.props),c.ref=er(f,N,h),c.return=f,f=c;break e}n(f,N);break}else t(f,N);N=N.sibling}h.type===pn?(c=Yt(h.props.children,f.mode,S,h.key),c.return=f,f=c):(S=wl(h.type,h.key,h.props,null,f.mode,S),S.ref=er(f,c,h),S.return=f,f=S)}return i(f);case dn:e:{for(N=h.key;c!==null;){if(c.key===N)if(c.tag===4&&c.stateNode.containerInfo===h.containerInfo&&c.stateNode.implementation===h.implementation){n(f,c.sibling),c=l(c,h.children||[]),c.return=f,f=c;break e}else{n(f,c);break}else t(f,c);c=c.sibling}c=Uo(h,f.mode,S),c.return=f,f=c}return i(f);case gt:return N=h._init,L(f,c,N(h._payload),S)}if(lr(h))return w(f,c,h,S);if(Xn(h))return k(f,c,h,S);tl(f,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,c!==null&&c.tag===6?(n(f,c.sibling),c=l(c,h),c.return=f,f=c):(n(f,c),c=Ao(h,f.mode,S),c.return=f,f=c),i(f)):n(f,c)}return L}var zn=$c(!0),Dc=$c(!1),Ur={},Je=$t(Ur),jr=$t(Ur),Lr=$t(Ur);function Kt(e){if(e===Ur)throw Error(E(174));return e}function Eu(e,t){switch(B(Lr,t),B(jr,e),B(Je,Ur),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:ri(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=ri(t,e)}V(Je),B(Je,t)}function $n(){V(Je),V(jr),V(Lr)}function Ic(e){Kt(Lr.current);var t=Kt(Je.current),n=ri(t,e.type);t!==n&&(B(jr,e),B(Je,n))}function xu(e){jr.current===e&&(V(Je),V(jr))}var G=$t(0);function $l(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var Fo=[];function Cu(){for(var e=0;en?n:4,e(!0);var r=Mo.transition;Mo.transition={};try{e(!1),t()}finally{I=n,Mo.transition=r}}function ef(){return $e().memoizedState}function $m(e,t,n){var r=Ot(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},tf(e))nf(t,n);else if(n=Pc(e,t,n,r),n!==null){var l=pe();He(n,e,r,l),rf(n,t,r)}}function Dm(e,t,n){var r=Ot(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(tf(e))nf(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,We(u,i)){var s=t.interleaved;s===null?(l.next=l,Su(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Pc(e,t,l,r),n!==null&&(l=pe(),He(n,e,r,l),rf(n,t,r))}}function tf(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function nf(e,t){pr=Dl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function rf(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,iu(e,n)}}var Il={readContext:ze,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},Im={readContext:ze,useCallback:function(e,t){return Ye().memoizedState=[e,t===void 0?null:t],e},useContext:ze,useEffect:Us,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,hl(4194308,4,Xc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return hl(4194308,4,e,t)},useInsertionEffect:function(e,t){return hl(4,2,e,t)},useMemo:function(e,t){var n=Ye();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ye();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=$m.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ye();return e={current:e},t.memoizedState=e},useState:As,useDebugValue:Lu,useDeferredValue:function(e){return Ye().memoizedState=e},useTransition:function(){var e=As(!1),t=e[0];return e=zm.bind(null,e[1]),Ye().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,l=Ye();if(Q){if(n===void 0)throw Error(E(407));n=n()}else{if(n=t(),ne===null)throw Error(E(349));qt&30||Bc(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Us(Wc.bind(null,r,o,e),[e]),r.flags|=2048,Pr(9,Hc.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ye(),t=ne.identifierPrefix;if(Q){var n=rt,r=nt;n=(r&~(1<<32-Be(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Rr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Xe]=t,e[Er]=r,cf(e,t,!1,!1),t.stateNode=e;e:{switch(i=ri(n,r),n){case"dialog":W("cancel",e),W("close",e),l=r;break;case"iframe":case"object":case"embed":W("load",e),l=r;break;case"video":case"audio":for(l=0;lMn&&(t.flags|=128,r=!0,Xn(o,!1),t.lanes=4194304)}else{if(!r)if(e=Pl(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Xn(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!Q)return se(t),null}else 2*J()-o.renderingStartTime>Mn&&n!==1073741824&&(t.flags|=128,r=!0,Xn(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=J(),t.sibling=null,n=G.current,B(G,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return Fu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ke&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(k(156,t.tag))}function Bm(e,t){switch(pu(t),t.tag){case 1:return we(t.type)&&Nl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Pn(),V(ge),V(ce),Eu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return ku(t),null;case 13:if(V(G),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(k(340));Ln()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(G),null;case 4:return Pn(),null;case 10:return yu(t.type._context),null;case 22:case 23:return Fu(),null;case 24:return null;default:return null}}var qr=!1,ae=!1,Hm=typeof WeakSet=="function"?WeakSet:Set,_=null;function wn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Z(e,t,r)}else n.current=null}function Li(e,t,n){try{n()}catch(r){Z(e,t,r)}}var Ks=!1;function Wm(e,t){if(pi=kl,e=hc(),fu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,a=0,d=0,h=e,p=null;t:for(;;){for(var g;h!==n||l!==0&&h.nodeType!==3||(u=i+l),h!==o||r!==0&&h.nodeType!==3||(s=i+r),h.nodeType===3&&(i+=h.nodeValue.length),(g=h.firstChild)!==null;)p=h,h=g;for(;;){if(h===e)break t;if(p===n&&++a===l&&(u=i),p===o&&++d===r&&(s=i),(g=h.nextSibling)!==null)break;h=p,p=h.parentNode}h=g}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(mi={focusedElem:e,selectionRange:n},kl=!1,_=t;_!==null;)if(t=_,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_=e;else for(;_!==null;){t=_;try{var S=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(S!==null){var E=S.memoizedProps,L=S.memoizedState,f=t.stateNode,c=f.getSnapshotBeforeUpdate(t.elementType===t.type?E:Ie(t.type,E),L);f.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var m=t.stateNode.containerInfo;m.nodeType===1?m.textContent="":m.nodeType===9&&m.documentElement&&m.removeChild(m.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(k(163))}}catch(w){Z(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,_=e;break}_=t.return}return S=Ks,Ks=!1,S}function ar(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Li(t,n,o)}l=l.next}while(l!==r)}}function eo(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Oi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function pf(e){var t=e.alternate;t!==null&&(e.alternate=null,pf(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Xe],delete t[Er],delete t[yi],delete t[Nm],delete t[Tm])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function mf(e){return e.tag===5||e.tag===3||e.tag===4}function Gs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||mf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Pi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Cl));else if(r!==4&&(e=e.child,e!==null))for(Pi(e,t,n),e=e.sibling;e!==null;)Pi(e,t,n),e=e.sibling}function Fi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Fi(e,t,n),e=e.sibling;e!==null;)Fi(e,t,n),e=e.sibling}var re=null,Ae=!1;function pt(e,t,n){for(n=n.child;n!==null;)hf(e,t,n),n=n.sibling}function hf(e,t,n){if(Ze&&typeof Ze.onCommitFiberUnmount=="function")try{Ze.onCommitFiberUnmount(Kl,n)}catch{}switch(n.tag){case 5:ae||wn(n,t);case 6:var r=re,l=Ae;re=null,pt(e,t,n),re=r,Ae=l,re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?jo(e.parentNode,n):e.nodeType===1&&jo(e,n),yr(e)):jo(re,n.stateNode));break;case 4:r=re,l=Ae,re=n.stateNode.containerInfo,Ae=!0,pt(e,t,n),re=r,Ae=l;break;case 0:case 11:case 14:case 15:if(!ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Li(n,t,i),l=l.next}while(l!==r)}pt(e,t,n);break;case 1:if(!ae&&(wn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Z(n,t,u)}pt(e,t,n);break;case 21:pt(e,t,n);break;case 22:n.mode&1?(ae=(r=ae)||n.memoizedState!==null,pt(e,t,n),ae=r):pt(e,t,n);break;default:pt(e,t,n)}}function Ys(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Hm),t.forEach(function(r){var l=qm.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function De(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Qm(r/1960))-r,10e?16:e,St===null)var r=!1;else{if(e=St,St=null,Dl=0,z&6)throw Error(k(331));var l=z;for(z|=4,_=e.current;_!==null;){var o=_,i=o.child;if(_.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sJ()-Ou?Vt(e,0):Lu|=n),Se(e,t)}function xf(e,t){t===0&&(e.mode&1?(t=Wr,Wr<<=1,!(Wr&130023424)&&(Wr=4194304)):t=1);var n=pe();e=ut(e,t),e!==null&&(Pr(e,t,n),Se(e,n))}function Jm(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),xf(e,n)}function qm(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(k(314))}r!==null&&r.delete(t),xf(e,n)}var Cf;Cf=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ge.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,Am(e,t,n);ye=!!(e.flags&131072)}else ye=!1,Q&&t.flags&1048576&&Tc(t,Rl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;dl(e,t),e=t.pendingProps;var l=jn(t,ce.current);Tn(t,n),l=Cu(null,t,r,e,l,n);var o=Nu();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,we(r)?(o=!0,Tl(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,wu(t),l.updater=ql,t.stateNode=l,l._reactInternals=t,xi(t,r,e,n),t=Ti(null,t,r,!0,o,n)):(t.tag=0,Q&&o&&du(t),de(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(dl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=eh(r),e=Ie(r,e),l){case 0:t=Ni(null,t,r,e,n);break e;case 1:t=Ws(null,t,r,e,n);break e;case 11:t=Bs(null,t,r,e,n);break e;case 14:t=Hs(null,t,r,Ie(r.type,e),n);break e}throw Error(k(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Ni(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Ws(e,t,r,l,n);case 3:e:{if(uf(t),e===null)throw Error(k(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Lc(e,t),Ol(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=Fn(Error(k(423)),t),t=Vs(e,t,r,n,l);break e}else if(r!==l){l=Fn(Error(k(424)),t),t=Vs(e,t,r,n,l);break e}else for(Ee=Ct(t.stateNode.containerInfo.firstChild),xe=t,Q=!0,Ue=null,n=Mc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ln(),r===l){t=st(e,t,n);break e}de(e,t,r,n)}t=t.child}return t;case 5:return zc(t),e===null&&Si(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,hi(r,l)?i=null:o!==null&&hi(r,o)&&(t.flags|=32),of(e,t),de(e,t,i,n),t.child;case 6:return e===null&&Si(t),null;case 13:return sf(e,t,n);case 4:return Su(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=On(t,null,r,n):de(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Bs(e,t,r,l,n);case 7:return de(e,t,t.pendingProps,n),t.child;case 8:return de(e,t,t.pendingProps.children,n),t.child;case 12:return de(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,B(jl,r._currentValue),r._currentValue=i,o!==null)if(We(o.value,i)){if(o.children===l.children&&!ge.current){t=st(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=rt(-1,n&-n),s.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var d=a.pending;d===null?s.next=s:(s.next=d.next,d.next=s),a.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),ki(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(k(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),ki(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}de(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Tn(t,n),l=ze(l),r=r(l),t.flags|=1,de(e,t,r,n),t.child;case 14:return r=t.type,l=Ie(r,t.pendingProps),l=Ie(r.type,l),Hs(e,t,r,l,n);case 15:return rf(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),dl(e,t),t.tag=1,we(r)?(e=!0,Tl(t)):e=!1,Tn(t,n),Pc(t,r,l),xi(t,r,l,n),Ti(null,t,r,!0,e,n);case 19:return af(e,t,n);case 22:return lf(e,t,n)}throw Error(k(156,t.tag))};function Nf(e,t){return Za(e,t)}function bm(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Fe(e,t,n,r){return new bm(e,t,n,r)}function zu(e){return e=e.prototype,!(!e||!e.isReactComponent)}function eh(e){if(typeof e=="function")return zu(e)?1:0;if(e!=null){if(e=e.$$typeof,e===eu)return 11;if(e===tu)return 14}return 2}function Rt(e,t){var n=e.alternate;return n===null?(n=Fe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function hl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")zu(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case cn:return Qt(n.children,l,o,t);case bi:i=8,l|=8;break;case Go:return e=Fe(12,n,t,l|2),e.elementType=Go,e.lanes=o,e;case Yo:return e=Fe(13,n,t,l),e.elementType=Yo,e.lanes=o,e;case Xo:return e=Fe(19,n,t,l),e.elementType=Xo,e.lanes=o,e;case Fa:return no(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Oa:i=10;break e;case Pa:i=9;break e;case eu:i=11;break e;case tu:i=14;break e;case mt:i=16,r=null;break e}throw Error(k(130,e==null?e:typeof e,""))}return t=Fe(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Qt(e,t,n,r){return e=Fe(7,e,r,t),e.lanes=n,e}function no(e,t,n,r){return e=Fe(22,e,r,t),e.elementType=Fa,e.lanes=n,e.stateNode={isHidden:!1},e}function Do(e,t,n){return e=Fe(6,e,null,t),e.lanes=n,e}function Io(e,t,n){return t=Fe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function th(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=go(0),this.expirationTimes=go(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=go(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function $u(e,t,n,r,l,o,i,u,s){return e=new th(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Fe(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},wu(o),e}function nh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(jf)}catch(e){console.error(e)}}jf(),Ta.exports=Ne;var Lf=Ta.exports;const kn=Vl(Lf);var na=Lf;Qo.createRoot=na.createRoot,Qo.hydrateRoot=na.hydrateRoot;var Of={exports:{}};/*! +`+o.stack}return{value:e,source:t,stack:l,digest:null}}function Do(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function Ti(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var Bm=typeof WeakMap=="function"?WeakMap:Map;function lf(e,t,n){n=lt(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){Ul||(Ul=!0,$i=r),Ti(e,t)},n}function of(e,t,n){n=lt(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){Ti(e,t)}}var o=e.stateNode;return o!==null&&typeof o.componentDidCatch=="function"&&(n.callback=function(){Ti(e,t),typeof r!="function"&&(Rt===null?Rt=new Set([this]):Rt.add(this));var i=t.stack;this.componentDidCatch(t.value,{componentStack:i!==null?i:""})}),n}function Bs(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Bm;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=th.bind(null,e,t,n),t.then(e,e))}function Hs(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Ws(e,t,n,r,l){return e.mode&1?(e.flags|=65536,e.lanes=l,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=lt(-1,1),t.tag=2,Lt(n,t,1))),n.lanes|=1),e)}var Hm=dt.ReactCurrentOwner,ye=!1;function de(e,t,n,r){t.child=e===null?Dc(t,null,n,r):zn(t,e.child,n,r)}function Vs(e,t,n,r,l){n=n.render;var o=t.ref;return Ln(t,l),r=Tu(e,t,n,r,o,l),n=_u(),e!==null&&!ye?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,at(e,t,l)):(Q&&n&&mu(t),t.flags|=1,de(e,t,r,l),t.child)}function Qs(e,t,n,r,l){if(e===null){var o=n.type;return typeof o=="function"&&!Du(o)&&o.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=o,uf(e,t,o,r,l)):(e=wl(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(o=e.child,!(e.lanes&l)){var i=o.memoizedProps;if(n=n.compare,n=n!==null?n:Cr,n(i,r)&&e.ref===t.ref)return at(e,t,l)}return t.flags|=1,e=Pt(o,r),e.ref=t.ref,e.return=t,t.child=e}function uf(e,t,n,r,l){if(e!==null){var o=e.memoizedProps;if(Cr(o,r)&&e.ref===t.ref)if(ye=!1,t.pendingProps=r=o,(e.lanes&l)!==0)e.flags&131072&&(ye=!0);else return t.lanes=e.lanes,at(e,t,l)}return _i(e,t,n,r,l)}function sf(e,t,n){var r=t.pendingProps,l=r.children,o=e!==null?e.memoizedState:null;if(r.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},B(xn,ke),ke|=n;else{if(!(n&1073741824))return e=o!==null?o.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,B(xn,ke),ke|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=o!==null?o.baseLanes:n,B(xn,ke),ke|=r}else o!==null?(r=o.baseLanes|n,t.memoizedState=null):r=n,B(xn,ke),ke|=r;return de(e,t,l,n),t.child}function af(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function _i(e,t,n,r,l){var o=we(n)?Zt:ce.current;return o=Fn(t,o),Ln(t,l),n=Tu(e,t,n,r,o,l),r=_u(),e!==null&&!ye?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,at(e,t,l)):(Q&&r&&mu(t),t.flags|=1,de(e,t,n,l),t.child)}function Ks(e,t,n,r,l){if(we(n)){var o=!0;Rl(t)}else o=!1;if(Ln(t,l),t.stateNode===null)vl(e,t),zc(t,n,r),Ni(t,n,r,l),r=!0;else if(e===null){var i=t.stateNode,u=t.memoizedProps;i.props=u;var s=i.context,a=n.contextType;typeof a=="object"&&a!==null?a=ze(a):(a=we(n)?Zt:ce.current,a=Fn(t,a));var d=n.getDerivedStateFromProps,m=typeof d=="function"||typeof i.getSnapshotBeforeUpdate=="function";m||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(u!==r||s!==a)&&Ds(t,i,r,a),wt=!1;var p=t.memoizedState;i.state=p,zl(t,r,i,l),s=t.memoizedState,u!==r||p!==s||ge.current||wt?(typeof d=="function"&&(Ci(t,n,d,r),s=t.memoizedState),(u=wt||$s(t,n,u,r,p,s,a))?(m||typeof i.UNSAFE_componentWillMount!="function"&&typeof i.componentWillMount!="function"||(typeof i.componentWillMount=="function"&&i.componentWillMount(),typeof i.UNSAFE_componentWillMount=="function"&&i.UNSAFE_componentWillMount()),typeof i.componentDidMount=="function"&&(t.flags|=4194308)):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=s),i.props=r,i.state=s,i.context=a,r=u):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{i=t.stateNode,Fc(e,t),u=t.memoizedProps,a=t.type===t.elementType?u:Ie(t.type,u),i.props=a,m=t.pendingProps,p=i.context,s=n.contextType,typeof s=="object"&&s!==null?s=ze(s):(s=we(n)?Zt:ce.current,s=Fn(t,s));var g=n.getDerivedStateFromProps;(d=typeof g=="function"||typeof i.getSnapshotBeforeUpdate=="function")||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(u!==m||p!==s)&&Ds(t,i,r,s),wt=!1,p=t.memoizedState,i.state=p,zl(t,r,i,l);var w=t.memoizedState;u!==m||p!==w||ge.current||wt?(typeof g=="function"&&(Ci(t,n,g,r),w=t.memoizedState),(a=wt||$s(t,n,a,r,p,w,s)||!1)?(d||typeof i.UNSAFE_componentWillUpdate!="function"&&typeof i.componentWillUpdate!="function"||(typeof i.componentWillUpdate=="function"&&i.componentWillUpdate(r,w,s),typeof i.UNSAFE_componentWillUpdate=="function"&&i.UNSAFE_componentWillUpdate(r,w,s)),typeof i.componentDidUpdate=="function"&&(t.flags|=4),typeof i.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof i.componentDidUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=w),i.props=r,i.state=w,i.context=s,r=a):(typeof i.componentDidUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),r=!1)}return ji(e,t,n,r,o,l)}function ji(e,t,n,r,l,o){af(e,t);var i=(t.flags&128)!==0;if(!r&&!i)return l&&Os(t,n,!1),at(e,t,o);r=t.stateNode,Hm.current=t;var u=i&&typeof n.getDerivedStateFromError!="function"?null:r.render();return t.flags|=1,e!==null&&i?(t.child=zn(t,e.child,null,o),t.child=zn(t,null,u,o)):de(e,t,u,o),t.memoizedState=r.state,l&&Os(t,n,!0),t.child}function cf(e){var t=e.stateNode;t.pendingContext?Rs(e,t.pendingContext,t.pendingContext!==t.context):t.context&&Rs(e,t.context,!1),Eu(e,t.containerInfo)}function Gs(e,t,n,r,l){return Mn(),vu(l),t.flags|=256,de(e,t,n,r),t.child}var Li={dehydrated:null,treeContext:null,retryLane:0};function Ri(e){return{baseLanes:e,cachePool:null,transitions:null}}function ff(e,t,n){var r=t.pendingProps,l=G.current,o=!1,i=(t.flags&128)!==0,u;if((u=i)||(u=e!==null&&e.memoizedState===null?!1:(l&2)!==0),u?(o=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(l|=1),B(G,l&1),e===null)return Ei(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?(t.mode&1?e.data==="$!"?t.lanes=8:t.lanes=1073741824:t.lanes=1,null):(i=r.children,e=r.fallback,o?(r=t.mode,o=t.child,i={mode:"hidden",children:i},!(r&1)&&o!==null?(o.childLanes=0,o.pendingProps=i):o=oo(i,r,0,null),e=Yt(e,r,n,null),o.return=t,e.return=t,o.sibling=e,t.child=o,t.child.memoizedState=Ri(n),t.memoizedState=Li,e):Ru(t,i));if(l=e.memoizedState,l!==null&&(u=l.dehydrated,u!==null))return Wm(e,t,i,r,u,l,n);if(o){o=r.fallback,i=t.mode,l=e.child,u=l.sibling;var s={mode:"hidden",children:r.children};return!(i&1)&&t.child!==l?(r=t.child,r.childLanes=0,r.pendingProps=s,t.deletions=null):(r=Pt(l,s),r.subtreeFlags=l.subtreeFlags&14680064),u!==null?o=Pt(u,o):(o=Yt(o,i,n,null),o.flags|=2),o.return=t,r.return=t,r.sibling=o,t.child=r,r=o,o=t.child,i=e.child.memoizedState,i=i===null?Ri(n):{baseLanes:i.baseLanes|n,cachePool:null,transitions:i.transitions},o.memoizedState=i,o.childLanes=e.childLanes&~n,t.memoizedState=Li,r}return o=e.child,e=o.sibling,r=Pt(o,{mode:"visible",children:r.children}),!(t.mode&1)&&(r.lanes=n),r.return=t,r.sibling=null,e!==null&&(n=t.deletions,n===null?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=r,t.memoizedState=null,r}function Ru(e,t){return t=oo({mode:"visible",children:t},e.mode,0,null),t.return=e,e.child=t}function nl(e,t,n,r){return r!==null&&vu(r),zn(t,e.child,null,n),e=Ru(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function Wm(e,t,n,r,l,o,i){if(n)return t.flags&256?(t.flags&=-257,r=Do(Error(E(422))),nl(e,t,i,r)):t.memoizedState!==null?(t.child=e.child,t.flags|=128,null):(o=r.fallback,l=t.mode,r=oo({mode:"visible",children:r.children},l,0,null),o=Yt(o,l,i,null),o.flags|=2,r.return=t,o.return=t,r.sibling=o,t.child=r,t.mode&1&&zn(t,e.child,null,i),t.child.memoizedState=Ri(i),t.memoizedState=Li,o);if(!(t.mode&1))return nl(e,t,i,null);if(l.data==="$!"){if(r=l.nextSibling&&l.nextSibling.dataset,r)var u=r.dgst;return r=u,o=Error(E(419)),r=Do(o,r,void 0),nl(e,t,i,r)}if(u=(i&e.childLanes)!==0,ye||u){if(r=ne,r!==null){switch(i&-i){case 4:l=2;break;case 16:l=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:l=32;break;case 536870912:l=268435456;break;default:l=0}l=l&(r.suspendedLanes|i)?0:l,l!==0&&l!==o.retryLane&&(o.retryLane=l,st(e,l),He(r,e,l,-1))}return $u(),r=Do(Error(E(421))),nl(e,t,i,r)}return l.data==="$?"?(t.flags|=128,t.child=e.child,t=nh.bind(null,e),l._reactRetry=t,null):(e=o.treeContext,Ee=jt(l.nextSibling),xe=t,Q=!0,Ue=null,e!==null&&(Re[Oe++]=nt,Re[Oe++]=rt,Re[Oe++]=Jt,nt=e.id,rt=e.overflow,Jt=t),t=Ru(t,r.children),t.flags|=4096,t)}function Ys(e,t,n){e.lanes|=t;var r=e.alternate;r!==null&&(r.lanes|=t),xi(e.return,t,n)}function Io(e,t,n,r,l){var o=e.memoizedState;o===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:l}:(o.isBackwards=t,o.rendering=null,o.renderingStartTime=0,o.last=r,o.tail=n,o.tailMode=l)}function df(e,t,n){var r=t.pendingProps,l=r.revealOrder,o=r.tail;if(de(e,t,r.children,n),r=G.current,r&2)r=r&1|2,t.flags|=128;else{if(e!==null&&e.flags&128)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Ys(e,n,t);else if(e.tag===19)Ys(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if(B(G,r),!(t.mode&1))t.memoizedState=null;else switch(l){case"forwards":for(n=t.child,l=null;n!==null;)e=n.alternate,e!==null&&$l(e)===null&&(l=n),n=n.sibling;n=l,n===null?(l=t.child,t.child=null):(l=n.sibling,n.sibling=null),Io(t,!1,l,n,o);break;case"backwards":for(n=null,l=t.child,t.child=null;l!==null;){if(e=l.alternate,e!==null&&$l(e)===null){t.child=l;break}e=l.sibling,l.sibling=n,n=l,l=e}Io(t,!0,n,null,o);break;case"together":Io(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function vl(e,t){!(t.mode&1)&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}function at(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),bt|=t.lanes,!(n&t.childLanes))return null;if(e!==null&&t.child!==e.child)throw Error(E(153));if(t.child!==null){for(e=t.child,n=Pt(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=Pt(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function Vm(e,t,n){switch(t.tag){case 3:cf(t),Mn();break;case 5:Ic(t);break;case 1:we(t.type)&&Rl(t);break;case 4:Eu(t,t.stateNode.containerInfo);break;case 10:var r=t.type._context,l=t.memoizedProps.value;B(Fl,r._currentValue),r._currentValue=l;break;case 13:if(r=t.memoizedState,r!==null)return r.dehydrated!==null?(B(G,G.current&1),t.flags|=128,null):n&t.child.childLanes?ff(e,t,n):(B(G,G.current&1),e=at(e,t,n),e!==null?e.sibling:null);B(G,G.current&1);break;case 19:if(r=(n&t.childLanes)!==0,e.flags&128){if(r)return df(e,t,n);t.flags|=128}if(l=t.memoizedState,l!==null&&(l.rendering=null,l.tail=null,l.lastEffect=null),B(G,G.current),r)break;return null;case 22:case 23:return t.lanes=0,sf(e,t,n)}return at(e,t,n)}var pf,Oi,mf,hf;pf=function(e,t){for(var n=t.child;n!==null;){if(n.tag===5||n.tag===6)e.appendChild(n.stateNode);else if(n.tag!==4&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}};Oi=function(){};mf=function(e,t,n,r){var l=e.memoizedProps;if(l!==r){e=t.stateNode,Kt(Je.current);var o=null;switch(n){case"input":l=bo(e,l),r=bo(e,r),o=[];break;case"select":l=X({},l,{value:void 0}),r=X({},r,{value:void 0}),o=[];break;case"textarea":l=ni(e,l),r=ni(e,r),o=[];break;default:typeof l.onClick!="function"&&typeof r.onClick=="function"&&(e.onclick=jl)}li(n,r);var i;n=null;for(a in l)if(!r.hasOwnProperty(a)&&l.hasOwnProperty(a)&&l[a]!=null)if(a==="style"){var u=l[a];for(i in u)u.hasOwnProperty(i)&&(n||(n={}),n[i]="")}else a!=="dangerouslySetInnerHTML"&&a!=="children"&&a!=="suppressContentEditableWarning"&&a!=="suppressHydrationWarning"&&a!=="autoFocus"&&(yr.hasOwnProperty(a)?o||(o=[]):(o=o||[]).push(a,null));for(a in r){var s=r[a];if(u=l!=null?l[a]:void 0,r.hasOwnProperty(a)&&s!==u&&(s!=null||u!=null))if(a==="style")if(u){for(i in u)!u.hasOwnProperty(i)||s&&s.hasOwnProperty(i)||(n||(n={}),n[i]="");for(i in s)s.hasOwnProperty(i)&&u[i]!==s[i]&&(n||(n={}),n[i]=s[i])}else n||(o||(o=[]),o.push(a,n)),n=s;else a==="dangerouslySetInnerHTML"?(s=s?s.__html:void 0,u=u?u.__html:void 0,s!=null&&u!==s&&(o=o||[]).push(a,s)):a==="children"?typeof s!="string"&&typeof s!="number"||(o=o||[]).push(a,""+s):a!=="suppressContentEditableWarning"&&a!=="suppressHydrationWarning"&&(yr.hasOwnProperty(a)?(s!=null&&a==="onScroll"&&W("scroll",e),o||u===s||(o=[])):(o=o||[]).push(a,s))}n&&(o=o||[]).push("style",n);var a=o;(t.updateQueue=a)&&(t.flags|=4)}};hf=function(e,t,n,r){n!==r&&(t.flags|=4)};function tr(e,t){if(!Q)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;n!==null;)n.alternate!==null&&(r=n),n=n.sibling;r===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:r.sibling=null}}function se(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,r=0;if(t)for(var l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags&14680064,r|=l.flags&14680064,l.return=e,l=l.sibling;else for(l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags,r|=l.flags,l.return=e,l=l.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function Qm(e,t,n){var r=t.pendingProps;switch(hu(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return se(t),null;case 1:return we(t.type)&&Ll(),se(t),null;case 3:return r=t.stateNode,$n(),V(ge),V(ce),Cu(),r.pendingContext&&(r.context=r.pendingContext,r.pendingContext=null),(e===null||e.child===null)&&(el(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&!(t.flags&256)||(t.flags|=1024,Ue!==null&&(Ai(Ue),Ue=null))),Oi(e,t),se(t),null;case 5:xu(t);var l=Kt(Lr.current);if(n=t.type,e!==null&&t.stateNode!=null)mf(e,t,n,r,l),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!r){if(t.stateNode===null)throw Error(E(166));return se(t),null}if(e=Kt(Je.current),el(t)){r=t.stateNode,n=t.type;var o=t.memoizedProps;switch(r[Xe]=t,r[_r]=o,e=(t.mode&1)!==0,n){case"dialog":W("cancel",r),W("close",r);break;case"iframe":case"object":case"embed":W("load",r);break;case"video":case"audio":for(l=0;l<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Xe]=t,e[_r]=r,pf(e,t,!1,!1),t.stateNode=e;e:{switch(i=oi(n,r),n){case"dialog":W("cancel",e),W("close",e),l=r;break;case"iframe":case"object":case"embed":W("load",e),l=r;break;case"video":case"audio":for(l=0;lIn&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304)}else{if(!r)if(e=$l(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),tr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!Q)return se(t),null}else 2*J()-o.renderingStartTime>In&&n!==1073741824&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=J(),t.sibling=null,n=G.current,B(G,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return zu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ke&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(E(156,t.tag))}function Km(e,t){switch(hu(t),t.tag){case 1:return we(t.type)&&Ll(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return $n(),V(ge),V(ce),Cu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return xu(t),null;case 13:if(V(G),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(E(340));Mn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(G),null;case 4:return $n(),null;case 10:return wu(t.type._context),null;case 22:case 23:return zu(),null;case 24:return null;default:return null}}var rl=!1,ae=!1,Gm=typeof WeakSet=="function"?WeakSet:Set,_=null;function En(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Z(e,t,r)}else n.current=null}function Pi(e,t,n){try{n()}catch(r){Z(e,t,r)}}var Xs=!1;function Ym(e,t){if(hi=Nl,e=gc(),pu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,a=0,d=0,m=e,p=null;t:for(;;){for(var g;m!==n||l!==0&&m.nodeType!==3||(u=i+l),m!==o||r!==0&&m.nodeType!==3||(s=i+r),m.nodeType===3&&(i+=m.nodeValue.length),(g=m.firstChild)!==null;)p=m,m=g;for(;;){if(m===e)break t;if(p===n&&++a===l&&(u=i),p===o&&++d===r&&(s=i),(g=m.nextSibling)!==null)break;m=p,p=m.parentNode}m=g}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(vi={focusedElem:e,selectionRange:n},Nl=!1,_=t;_!==null;)if(t=_,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_=e;else for(;_!==null;){t=_;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var k=w.memoizedProps,L=w.memoizedState,f=t.stateNode,c=f.getSnapshotBeforeUpdate(t.elementType===t.type?k:Ie(t.type,k),L);f.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(E(163))}}catch(S){Z(t,t.return,S)}if(e=t.sibling,e!==null){e.return=t.return,_=e;break}_=t.return}return w=Xs,Xs=!1,w}function mr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Pi(t,n,o)}l=l.next}while(l!==r)}}function ro(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Fi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function vf(e){var t=e.alternate;t!==null&&(e.alternate=null,vf(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Xe],delete t[_r],delete t[wi],delete t[Rm],delete t[Om])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function yf(e){return e.tag===5||e.tag===3||e.tag===4}function Zs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||yf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Mi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=jl));else if(r!==4&&(e=e.child,e!==null))for(Mi(e,t,n),e=e.sibling;e!==null;)Mi(e,t,n),e=e.sibling}function zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(zi(e,t,n),e=e.sibling;e!==null;)zi(e,t,n),e=e.sibling}var re=null,Ae=!1;function ht(e,t,n){for(n=n.child;n!==null;)gf(e,t,n),n=n.sibling}function gf(e,t,n){if(Ze&&typeof Ze.onCommitFiberUnmount=="function")try{Ze.onCommitFiberUnmount(Xl,n)}catch{}switch(n.tag){case 5:ae||En(n,t);case 6:var r=re,l=Ae;re=null,ht(e,t,n),re=r,Ae=l,re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?Oo(e.parentNode,n):e.nodeType===1&&Oo(e,n),Er(e)):Oo(re,n.stateNode));break;case 4:r=re,l=Ae,re=n.stateNode.containerInfo,Ae=!0,ht(e,t,n),re=r,Ae=l;break;case 0:case 11:case 14:case 15:if(!ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Pi(n,t,i),l=l.next}while(l!==r)}ht(e,t,n);break;case 1:if(!ae&&(En(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Z(n,t,u)}ht(e,t,n);break;case 21:ht(e,t,n);break;case 22:n.mode&1?(ae=(r=ae)||n.memoizedState!==null,ht(e,t,n),ae=r):ht(e,t,n);break;default:ht(e,t,n)}}function Js(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Gm),t.forEach(function(r){var l=rh.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function De(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Zm(r/1960))-r,10e?16:e,Ct===null)var r=!1;else{if(e=Ct,Ct=null,Bl=0,z&6)throw Error(E(331));var l=z;for(z|=4,_=e.current;_!==null;){var o=_,i=o.child;if(_.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sJ()-Fu?Gt(e,0):Pu|=n),Se(e,t)}function Tf(e,t){t===0&&(e.mode&1?(t=Yr,Yr<<=1,!(Yr&130023424)&&(Yr=4194304)):t=1);var n=pe();e=st(e,t),e!==null&&(Dr(e,t,n),Se(e,n))}function nh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Tf(e,n)}function rh(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(E(314))}r!==null&&r.delete(t),Tf(e,n)}var _f;_f=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ge.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,Vm(e,t,n);ye=!!(e.flags&131072)}else ye=!1,Q&&t.flags&1048576&&Lc(t,Pl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;vl(e,t),e=t.pendingProps;var l=Fn(t,ce.current);Ln(t,n),l=Tu(null,t,r,e,l,n);var o=_u();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,we(r)?(o=!0,Rl(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ku(t),l.updater=to,t.stateNode=l,l._reactInternals=t,Ni(t,r,e,n),t=ji(null,t,r,!0,o,n)):(t.tag=0,Q&&o&&mu(t),de(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(vl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=oh(r),e=Ie(r,e),l){case 0:t=_i(null,t,r,e,n);break e;case 1:t=Ks(null,t,r,e,n);break e;case 11:t=Vs(null,t,r,e,n);break e;case 14:t=Qs(null,t,r,Ie(r.type,e),n);break e}throw Error(E(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),_i(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Ks(e,t,r,l,n);case 3:e:{if(cf(t),e===null)throw Error(E(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Fc(e,t),zl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=Dn(Error(E(423)),t),t=Gs(e,t,r,n,l);break e}else if(r!==l){l=Dn(Error(E(424)),t),t=Gs(e,t,r,n,l);break e}else for(Ee=jt(t.stateNode.containerInfo.firstChild),xe=t,Q=!0,Ue=null,n=Dc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Mn(),r===l){t=at(e,t,n);break e}de(e,t,r,n)}t=t.child}return t;case 5:return Ic(t),e===null&&Ei(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,yi(r,l)?i=null:o!==null&&yi(r,o)&&(t.flags|=32),af(e,t),de(e,t,i,n),t.child;case 6:return e===null&&Ei(t),null;case 13:return ff(e,t,n);case 4:return Eu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=zn(t,null,r,n):de(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Vs(e,t,r,l,n);case 7:return de(e,t,t.pendingProps,n),t.child;case 8:return de(e,t,t.pendingProps.children,n),t.child;case 12:return de(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,B(Fl,r._currentValue),r._currentValue=i,o!==null)if(We(o.value,i)){if(o.children===l.children&&!ge.current){t=at(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=lt(-1,n&-n),s.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var d=a.pending;d===null?s.next=s:(s.next=d.next,d.next=s),a.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),xi(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(E(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),xi(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}de(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Ln(t,n),l=ze(l),r=r(l),t.flags|=1,de(e,t,r,n),t.child;case 14:return r=t.type,l=Ie(r,t.pendingProps),l=Ie(r.type,l),Qs(e,t,r,l,n);case 15:return uf(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),vl(e,t),t.tag=1,we(r)?(e=!0,Rl(t)):e=!1,Ln(t,n),zc(t,r,l),Ni(t,r,l,n),ji(null,t,r,!0,e,n);case 19:return df(e,t,n);case 22:return sf(e,t,n)}throw Error(E(156,t.tag))};function jf(e,t){return ba(e,t)}function lh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Fe(e,t,n,r){return new lh(e,t,n,r)}function Du(e){return e=e.prototype,!(!e||!e.isReactComponent)}function oh(e){if(typeof e=="function")return Du(e)?1:0;if(e!=null){if(e=e.$$typeof,e===nu)return 11;if(e===ru)return 14}return 2}function Pt(e,t){var n=e.alternate;return n===null?(n=Fe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function wl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Du(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case pn:return Yt(n.children,l,o,t);case tu:i=8,l|=8;break;case Xo:return e=Fe(12,n,t,l|2),e.elementType=Xo,e.lanes=o,e;case Zo:return e=Fe(13,n,t,l),e.elementType=Zo,e.lanes=o,e;case Jo:return e=Fe(19,n,t,l),e.elementType=Jo,e.lanes=o,e;case $a:return oo(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ma:i=10;break e;case za:i=9;break e;case nu:i=11;break e;case ru:i=14;break e;case gt:i=16,r=null;break e}throw Error(E(130,e==null?e:typeof e,""))}return t=Fe(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Yt(e,t,n,r){return e=Fe(7,e,r,t),e.lanes=n,e}function oo(e,t,n,r){return e=Fe(22,e,r,t),e.elementType=$a,e.lanes=n,e.stateNode={isHidden:!1},e}function Ao(e,t,n){return e=Fe(6,e,null,t),e.lanes=n,e}function Uo(e,t,n){return t=Fe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function ih(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=So(0),this.expirationTimes=So(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=So(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Iu(e,t,n,r,l,o,i,u,s){return e=new ih(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Fe(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ku(o),e}function uh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Pf)}catch(e){console.error(e)}}Pf(),La.exports=Ne;var Ff=La.exports;const Cn=Gl(Ff);var oa=Ff;Go.createRoot=oa.createRoot,Go.hydrateRoot=oa.hydrateRoot;var Mf={exports:{}};/*! Copyright (c) 2018 Jed Watson. Licensed under the MIT License (MIT), see http://jedwatson.github.io/classnames -*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ra(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function sh(e){var t=ah(e,"string");return typeof t=="symbol"?t:String(t)}function ah(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function ch(e,t,n){var r=v.useRef(e!==void 0),l=v.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,v.useCallback(function(a){for(var d=arguments.length,h=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Mh(e){e.offsetHeight}const ia=e=>!e||typeof e=="function"?e:t=>{e.current=t};function zh(e,t){const n=ia(e),r=ia(t);return l=>{n&&n(l),r&&r(l)}}function ao(e,t){return v.useMemo(()=>zh(e,t),[e,t])}function $h(e){return e&&"setState"in e?kn.findDOMNode(e):e??null}const Dh=Ut.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const h=v.useRef(null),p=ao(h,s),g=N=>{p($h(N))},S=N=>T=>{N&&h.current&&N(h.current,T)},E=v.useCallback(S(e),[e]),L=v.useCallback(S(t),[t]),f=v.useCallback(S(n),[n]),c=v.useCallback(S(r),[r]),m=v.useCallback(S(l),[l]),w=v.useCallback(S(o),[o]),C=v.useCallback(S(i),[i]);return y.jsx(jh,{ref:d,...a,onEnter:E,onEntered:f,onEntering:L,onExit:c,onExited:w,onExiting:m,addEndListener:C,nodeRef:h,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Ut.cloneElement(u,{ref:g})})}),Ih=Dh;function Ah(e){const t=v.useRef(e);return v.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Ah(e);return v.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Bf=e=>v.forwardRef((t,n)=>y.jsx("div",{...t,ref:n,className:M(t.className,e)})),Hf=Bf("h4");Hf.displayName="DivStyledAsH4";const Wf=v.forwardRef(({className:e,bsPrefix:t,as:n=Hf,...r},l)=>(t=H(t,"alert-heading"),y.jsx(n,{ref:l,className:M(e,t),...r})));Wf.displayName="AlertHeading";const Uh=Wf;function Bh(){return v.useState(null)}function Hh(){const e=v.useRef(!0),t=v.useRef(()=>e.current);return v.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Wh(e){const t=v.useRef(null);return v.useEffect(()=>{t.current=e}),t.current}const Vh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Qh=typeof document<"u",ua=Qh||Vh?v.useLayoutEffect:v.useEffect,Kh=["as","disabled"];function Gh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function Yh(e){return!e||e.trim()==="#"}function Uu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&Yh(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},h=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:h},a]}const Xh=v.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=Gh(e,Kh);const[o,{tagName:i}]=Uu(Object.assign({tagName:n,disabled:r},l));return y.jsx(i,Object.assign({},l,o,{ref:t}))});Xh.displayName="Button";const Zh=["onKeyDown"];function Jh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function qh(e){return!e||e.trim()==="#"}const Vf=v.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=Jh(e,Zh);const[l]=Uu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return qh(r.href)||r.role==="button"?y.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):y.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Vf.displayName="Anchor";const bh=Vf,Qf=v.forwardRef(({className:e,bsPrefix:t,as:n=bh,...r},l)=>(t=H(t,"alert-link"),y.jsx(n,{ref:l,className:M(e,t),...r})));Qf.displayName="AlertLink";const ev=Qf,tv={[vt]:"show",[At]:"show"},Kf=v.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=v.useCallback((s,a)=>{Mh(s),r==null||r(s,a)},[r]);return y.jsx(Ih,{ref:o,addEndListener:Fh,...i,onEnter:u,childRef:t.ref,children:(s,a)=>v.cloneElement(t,{...a,className:M("fade",e,t.props.className,tv[s],n[s])})})});Kf.displayName="Fade";const Bl=Kf,nv={"aria-label":lt.string,onClick:lt.func,variant:lt.oneOf(["white"])},Bu=v.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>y.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Bu.displayName="CloseButton";Bu.propTypes=nv;const Gf=Bu,Yf=v.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:h=Bl,...p}=fh(e,{show:"onClose"}),g=H(n,"alert"),S=Pe(f=>{a&&a(!1,f)}),E=h===!0?Bl:h,L=y.jsxs("div",{role:"alert",...E?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&y.jsx(Gf,{onClick:S,"aria-label":l,variant:o}),u]});return E?y.jsx(E,{unmountOnExit:!0,...p,ref:void 0,in:r,children:L}):r?L:null});Yf.displayName="Alert";const sa=Object.assign(Yf,{Link:ev,Heading:Uh}),Xf=v.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:h}]=Uu({tagName:e,disabled:o,...u}),p=h;return y.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});Xf.displayName="Button";const Vi=Xf;function rv(e){const t=v.useRef(e);return t.current=e,t}function Zf(e){const t=rv(e);v.useEffect(()=>()=>t.current(),[])}function lv(e,t){let n=0;return v.Children.map(e,r=>v.isValidElement(r)?t(r,n++):r)}function ov(e,t){return v.Children.toArray(e).some(n=>v.isValidElement(n)&&n.type===t)}function iv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=Ff(),o=Mf(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,h,p;typeof a=="object"&&a!=null?{span:d,offset:h,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),h!=null&&u.push(`offset${g}-${h}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const Jf=v.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=iv(e);return y.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});Jf.displayName="Col";const qf=Jf,bf=v.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return y.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});bf.displayName="Container";const uv=bf;var sv=Function.prototype.bind.call(Function.prototype.call,[].slice);function un(e,t){return sv(e.querySelectorAll(t))}function aa(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const av="data-rr-ui-";function cv(e){return`${av}${e}`}const ed=v.createContext(In?window:void 0);ed.Provider;function Hu(){return v.useContext(ed)}const fv={type:lt.string,tooltip:lt.bool,as:lt.elementType},Wu=v.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>y.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Wu.displayName="Feedback";Wu.propTypes=fv;const td=Wu,dv=v.createContext({}),at=dv,nd=v.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=v.useContext(at);return t=H(t,"form-check-input"),y.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});nd.displayName="FormCheckInput";const rd=nd,ld=v.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=v.useContext(at);return e=H(e,"form-check-label"),y.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});ld.displayName="FormCheckLabel";const Qi=ld,od=v.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:h,style:p,title:g="",type:S="checkbox",label:E,children:L,as:f="input",...c},m)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:w}=v.useContext(at),C=v.useMemo(()=>({controlId:e||w}),[w,e]),N=!L&&E!=null&&E!==!1||ov(L,Qi),T=y.jsx(rd,{...c,type:S==="switch"?"checkbox":S,ref:m,isValid:i,isInvalid:u,disabled:o,as:f});return y.jsx(at.Provider,{value:C,children:y.jsx("div",{style:p,className:M(h,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,S==="switch"&&n),children:L||y.jsxs(y.Fragment,{children:[T,N&&y.jsx(Qi,{title:g,children:E}),a&&y.jsx(td,{type:d,tooltip:s,children:a})]})})})});od.displayName="FormCheck";const Hl=Object.assign(od,{Input:rd,Label:Qi}),id=v.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...h},p)=>{const{controlId:g}=v.useContext(at);return e=H(e,"form-control"),y.jsx(d,{...h,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});id.displayName="FormControl";const pv=Object.assign(id,{Feedback:td}),ud=v.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),y.jsx(n,{ref:l,className:M(e,t),...r})));ud.displayName="FormFloating";const mv=ud,sd=v.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=v.useMemo(()=>({controlId:e}),[e]);return y.jsx(at.Provider,{value:l,children:y.jsx(t,{...n,ref:r})})});sd.displayName="FormGroup";const ad=sd,cd=v.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=v.useContext(at);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?y.jsx(qf,{ref:u,as:"label",className:d,htmlFor:o,...i}):y.jsx(e,{ref:u,className:d,htmlFor:o,...i})});cd.displayName="FormLabel";const hv=cd,fd=v.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=v.useContext(at);return e=H(e,"form-range"),y.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});fd.displayName="FormRange";const vv=fd,dd=v.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=v.useContext(at);return e=H(e,"form-select"),y.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});dd.displayName="FormSelect";const yv=dd,pd=v.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),y.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));pd.displayName="FormText";const gv=pd,md=v.forwardRef((e,t)=>y.jsx(Hl,{...e,ref:t,type:"switch"}));md.displayName="Switch";const wv=Object.assign(md,{Input:Hl.Input,Label:Hl.Label}),hd=v.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),y.jsxs(ad,{ref:i,className:M(t,e),controlId:r,...o,children:[n,y.jsx("label",{htmlFor:r,children:l})]})));hd.displayName="FloatingLabel";const Sv=hd,kv={_ref:lt.any,validated:lt.bool,as:lt.elementType},Vu=v.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>y.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Vu.displayName="Form";Vu.propTypes=kv;const Uo=Object.assign(Vu,{Group:ad,Control:pv,Floating:mv,Check:Hl,Switch:wv,Label:hv,Text:gv,Range:vv,Select:yv,FloatingLabel:Sv});var nl;function ca(e){if((!nl&&nl!==0||e)&&In){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),nl=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return nl}function Bo(e){e===void 0&&(e=so());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function Ev(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const fa=cv("modal-open");class xv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return Ev(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Kt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(fa,""),Kt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(fa),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Qu=xv,Ho=(e,t)=>In?e==null?(t||so()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Cv(e,t){const n=Hu(),[r,l]=v.useState(()=>Ho(e,n==null?void 0:n.document));if(!r){const o=Ho(e);o&&l(o)}return v.useEffect(()=>{t&&r&&t(r)},[t,r]),v.useEffect(()=>{const o=Ho(e);o!==r&&l(o)},[e,r]),r}function Nv({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=v.useRef(null),i=v.useRef(t),u=Pe(n);v.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=ao(o,e.ref),a=v.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Tv({in:e,onTransition:t}){const n=v.useRef(null),r=v.useRef(!0),l=Pe(t);return ua(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ua(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function _v({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=v.useState(!t);t&&o&&i(!1);const u=Tv({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,h=>{throw a.in||i(!0),h})}}),s=ao(u,e.ref);return o&&!t?null:v.cloneElement(e,{ref:s})}function da(e,t,n){return e?y.jsx(e,Object.assign({},n)):t?y.jsx(_v,Object.assign({},n,{transition:t})):y.jsx(Nv,Object.assign({},n))}function Rv(e){return e.code==="Escape"||e.keyCode===27}const jv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function Lv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Wo;function Ov(e){return Wo||(Wo=new Qu({ownerDocument:e==null?void 0:e.document})),Wo}function Pv(e){const t=Hu(),n=e||Ov(t),r=v.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:v.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:v.useCallback(l=>{r.current.backdrop=l},[])})}const vd=v.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:h,runTransition:p,backdropTransition:g,runBackdropTransition:S,autoFocus:E=!0,enforceFocus:L=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:m,renderBackdrop:w=K=>y.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:R=()=>{},onExit:U,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:en}=e,An=Lv(e,jv);const _e=Hu(),Ke=Cv(N),x=Pv(C),j=Hh(),O=Wh(n),[D,A]=v.useState(!n),fe=v.useRef(null);v.useImperativeHandle(t,()=>x,[x]),In&&!O&&n&&(fe.current=Bo(_e==null?void 0:_e.document)),n&&D&&A(!1);const Re=Pe(()=>{if(x.add(),nn.current=Ul(document,"keydown",fo),tn.current=Ul(document,"focus",()=>setTimeout(je),!0),T&&T(),E){var K,Dr;const Hn=Bo((K=(Dr=x.dialog)==null?void 0:Dr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Hn&&!aa(x.dialog,Hn)&&(fe.current=Hn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),nn.current==null||nn.current(),tn.current==null||tn.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});v.useEffect(()=>{!n||!Ke||Re()},[n,Ke,Re]),v.useEffect(()=>{D&&qe()},[D,qe]),Zf(()=>{qe()});const je=Pe(()=>{if(!L||!j()||!x.isTopModal())return;const K=Bo(_e==null?void 0:_e.document);x.dialog&&K&&!aa(x.dialog,K)&&x.dialog.focus()}),dt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&R())}),fo=Pe(K=>{s&&Rv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||R())}),tn=v.useRef(),nn=v.useRef(),Un=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const $r=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},An,{style:o,className:l,tabIndex:-1});let Bn=m?m($r):y.jsx("div",Object.assign({},$r,{children:v.cloneElement(i,{role:"document"})}));Bn=da(h,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:U,onExiting:ie,onExited:Un,onEnter:Ve,onEntering:Qe,onEntered:en,children:Bn});let Mt=null;return u&&(Mt=w({ref:x.setBackdropRef,onClick:dt}),Mt=da(g,S,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:Mt})),y.jsx(y.Fragment,{children:kn.createPortal(y.jsxs(y.Fragment,{children:[Mt,Bn]}),Ke)})});vd.displayName="Modal";const Fv=Object.assign(vd,{Manager:Qu});function Mv(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function zv(e,t){e.classList?e.classList.add(t):Mv(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function pa(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function $v(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=pa(e.className,t):e.setAttribute("class",pa(e.className&&e.className.baseVal||"",t))}const sn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Dv extends Qu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Kt(n,{[t]:`${parseFloat(Kt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Kt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(zv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";un(n,sn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),un(n,sn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),un(n,sn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();$v(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";un(n,sn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),un(n,sn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),un(n,sn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Vo;function Iv(e){return Vo||(Vo=new Dv(e)),Vo}const yd=v.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),y.jsx(n,{ref:l,className:M(e,t),...r})));yd.displayName="ModalBody";const Av=yd,Uv=v.createContext({onHide(){}}),gd=Uv,wd=v.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,h=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return y.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&h),children:y.jsx("div",{className:M(`${e}-content`,n),children:i})})});wd.displayName="ModalDialog";const Sd=wd,kd=v.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),y.jsx(n,{ref:l,className:M(e,t),...r})));kd.displayName="ModalFooter";const Bv=kd,Hv=v.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=v.useContext(gd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return y.jsxs("div",{ref:i,...o,children:[l,n&&y.jsx(Gf,{"aria-label":e,variant:t,onClick:s})]})}),Wv=Hv,Ed=v.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),y.jsx(Wv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Ed.displayName="ModalHeader";const Vv=Ed,Qv=Bf("h4"),xd=v.forwardRef(({className:e,bsPrefix:t,as:n=Qv,...r},l)=>(t=H(t,"modal-title"),y.jsx(n,{ref:l,className:M(e,t),...r})));xd.displayName="ModalTitle";const Kv=xd;function Gv(e){return y.jsx(Bl,{...e,timeout:null})}function Yv(e){return y.jsx(Bl,{...e,timeout:null})}const Cd=v.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=Sd,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:h=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:S,onShow:E,onHide:L,container:f,autoFocus:c=!0,enforceFocus:m=!0,restoreFocus:w=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:R,onEnter:U,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...en},An)=>{const[_e,Ke]=v.useState({}),[x,j]=v.useState(!1),O=v.useRef(!1),D=v.useRef(!1),A=v.useRef(null),[fe,Re]=Bh(),qe=ao(An,Re),je=Pe(L),dt=hh();e=H(e,"modal");const fo=v.useMemo(()=>({onHide:je}),[je]);function tn(){return Qe||Iv({isRTL:dt})}function nn($){if(!In)return;const rn=tn().getScrollbarWidth()>0,Gu=$.scrollHeight>so($).documentElement.clientHeight;Ke({paddingRight:rn&&!Gu?ca():void 0,paddingLeft:!rn&&Gu?ca():void 0})}const Un=Pe(()=>{fe&&nn(fe.dialog)});Zf(()=>{Wi(window,"resize",Un),A.current==null||A.current()});const $r=()=>{O.current=!0},Bn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},Mt=()=>{j(!0),A.current=Uf(fe.dialog,()=>{j(!1)})},K=$=>{$.target===$.currentTarget&&Mt()},Dr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}L==null||L()},Hn=$=>{g?S==null||S($):($.preventDefault(),p==="static"&&Mt())},Od=($,rn)=>{$&&nn($),U==null||U($,rn)},Pd=$=>{A.current==null||A.current(),T==null||T($)},Fd=($,rn)=>{P==null||P($,rn),Af(window,"resize",Un)},Md=$=>{$&&($.style.display=""),ie==null||ie($),Wi(window,"resize",Un)},zd=v.useCallback($=>y.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!h&&"show")}),[h,Ve,e]),Ku={...n,..._e};Ku.display="block";const $d=$=>y.jsx("div",{role:"dialog",...$,style:Ku,className:M(t,e,x&&`${e}-static`,!h&&"show"),onClick:p?Dr:void 0,onMouseUp:Bn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:y.jsx(i,{...en,onMouseDown:$r,className:r,contentClassName:l,children:o})});return y.jsx(gd.Provider,{value:fo,children:y.jsx(Fv,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:m,restoreFocus:w,restoreFocusOptions:C,onEscapeKeyDown:Hn,onShow:E,onHide:L,onEnter:Od,onEntering:Fd,onEntered:N,onExit:Pd,onExiting:R,onExited:Md,manager:tn(),transition:h?Gv:void 0,backdropTransition:h?Yv:void 0,renderBackdrop:zd,renderDialog:$d})})});Cd.displayName="Modal";const Jn=Object.assign(Cd,{Body:Av,Header:Vv,Title:Kv,Footer:Bv,Dialog:Sd,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ma=1e3;function Xv(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ma)/ma}function ha({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...h},p){return y.jsx("div",{ref:p,...h,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${Xv(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?y.jsx("span",{className:"visually-hidden",children:r}):r})}const Nd=v.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ha(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:h,variant:p,className:g,children:S,...E}=r;return y.jsx("div",{ref:n,...E,className:M(g,h),children:S?lv(S,L=>v.cloneElement(L,{isChild:!0})):ha({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:h,variant:p},n)})});Nd.displayName="ProgressBar";const Zv=Nd,Td=v.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=Ff(),u=Mf(),s=`${o}-cols`,a=[];return i.forEach(d=>{const h=r[d];delete r[d];let p;h!=null&&typeof h=="object"?{cols:p}=h:p=h;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),y.jsx(n,{ref:l,...r,className:M(t,o,...a)})});Td.displayName="Row";const Jv=Td,_d=v.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return y.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});_d.displayName="Spinner";const jr=_d,qv=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",rl=async(e,t,n)=>{console.log(e,t);const r=qv+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},Lr={listTorrents:()=>rl("GET","/torrents"),getTorrentDetails:e=>rl("GET",`/torrents/${e}`),getTorrentStats:e=>rl("GET",`/torrents/${e}/stats`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),rl("POST",n,e)}},co=v.createContext(null),bv=({id:e,detailsResponse:t,statsResponse:n})=>{var u,s;const r=((u=n==null?void 0:n.snapshot)==null?void 0:u.total_bytes)??1,l=((s=n==null?void 0:n.snapshot)==null?void 0:s.have_bytes)??0,o=r==l,i=l/r*100;return y.jsxs(Jv,{className:`${e%2==0?"bg-light":""}`,children:[y.jsx(zt,{size:4,label:"Name",children:t?y.jsx("div",{className:"text-truncate",children:cy(t)}):y.jsx(jr,{})}),n?y.jsxs(y.Fragment,{children:[y.jsx(zt,{label:"Size",children:`${jd(r)} `}),y.jsx(zt,{size:2,label:"Progress",children:y.jsx(Zv,{now:i,label:`${i.toFixed(2)}% `,animated:!o})}),y.jsx(zt,{size:2,label:"Down Speed",children:n.download_speed.human_readable}),y.jsx(zt,{label:"ETA",children:fy(n)}),y.jsx(zt,{size:2,label:"Peers",children:`${n.snapshot.peer_stats.live} / ${n.snapshot.peer_stats.seen}`})]}):y.jsx(zt,{label:"Loading stats",size:8,children:y.jsx(jr,{})})]})},zt=({size:e,label:t,children:n})=>y.jsxs(qf,{md:e||1,className:"py-3",children:[y.jsx("div",{className:"fw-bold",children:t}),n]}),ey=({id:e,torrent:t})=>{const[n,r]=v.useState(null),[l,o]=v.useState(null);return v.useEffect(()=>{if(n===null)return py(async()=>{await Lr.getTorrentDetails(t.id).then(r)},1e3)},[n]),v.useEffect(()=>Ld(async()=>Lr.getTorrentStats(t.id).then(a=>(o(a),ay(a)?5e3:500),a=>1e4),0),[]),y.jsx(bv,{id:e,detailsResponse:n,statsResponse:l})},ty=e=>{if(e.torrents===null&&e.loading)return y.jsx(jr,{});if(e.torrents!==null)return e.torrents.length===0?y.jsx("div",{className:"text-center",children:y.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):y.jsx(y.Fragment,{children:e.torrents.map(t=>y.jsx(ey,{id:t.id,torrent:t},t.id))})},ny=()=>{const[e,t]=v.useState(null),[n,r]=v.useState(null),[l,o]=v.useState(null),[i,u]=v.useState(!1),s=async()=>{u(!0);let d=await Lr.listTorrents().finally(()=>u(!1));o(d.torrents)};v.useEffect(()=>Ld(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return y.jsx(co.Provider,{value:a,children:y.jsxs("div",{className:"text-center",children:[y.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 0.0.1-alpha"}),y.jsx(sy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},ry=e=>{let{details:t}=e;return t?y.jsxs(y.Fragment,{children:[t.status&&y.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},Wl=e=>{let{error:t,remove:n}=e;return t==null?null:y.jsxs(sa,{variant:"danger",onClose:n,dismissible:n!=null,children:[y.jsx(sa.Heading,{children:t.text}),y.jsx(ry,{details:t.details})]})},Rd=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=v.useState(!1),[u,s]=v.useState([]),[a,d]=v.useState(null);v.useContext(co);const h=n!==null||a!==null;v.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const S=await Lr.uploadTorrent(n,{listOnly:!0});s(S.details.files)}catch(S){d({text:"Error listing torrent",details:S})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return y.jsxs(y.Fragment,{children:[y.jsx(Vi,{variant:l,onClick:t,className:"m-1",children:e}),y.jsx(iy,{show:h,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},ly=()=>{let[e,t]=v.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return y.jsx(Rd,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},oy=()=>{const e=v.useRef(),[t,n]=v.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return y.jsxs(y.Fragment,{children:[y.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),y.jsx(Rd,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},iy=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=v.useState([]),[a,d]=v.useState(!1),[h,p]=v.useState(null),g=v.useContext(co);v.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const S=()=>{n(),s([]),p(null),d(!1)},E=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},L=async()=>{d(!0),Lr.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return y.jsxs(Jn,{show:t,onHide:S,size:"lg",children:[y.jsx(Jn.Header,{closeButton:!0,children:!!l||y.jsx(Jn.Title,{children:"Select Files"})}),y.jsxs(Jn.Body,{children:[o?y.jsx(jr,{}):l?y.jsx(Wl,{error:l}):y.jsx(Uo,{children:r.map((f,c)=>y.jsx(Uo.Group,{controlId:`check-${c}`,children:y.jsx(Uo.Check,{type:"checkbox",label:`${f.name} (${jd(f.length)})`,checked:u.includes(c),onChange:()=>E(c)})},c))}),y.jsx(Wl,{error:h})]}),y.jsxs(Jn.Footer,{children:[a&&y.jsx(jr,{}),y.jsx(Vi,{variant:"primary",onClick:L,disabled:o||a||u.length==0,children:"OK"}),y.jsx(Vi,{variant:"secondary",onClick:S,children:"Cancel"})]})]})},uy=()=>y.jsxs("div",{id:"buttons-container",className:"mt-3",children:[y.jsx(ly,{}),y.jsx(oy,{})]}),sy=e=>{let t=v.useContext(co);return y.jsxs(uv,{children:[y.jsx(Wl,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),y.jsx(Wl,{error:e.otherError}),y.jsx(ty,{torrents:e.torrents,loading:e.torrentsLoading}),y.jsx(uy,{})]})};function ay(e){return e.snapshot.have_bytes==e.snapshot.total_bytes}function jd(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function cy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function fy(e){return e.time_remaining&&e.time_remaining.duration?dy(e.time_remaining.duration.secs):"N/A"}function dy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function Ld(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function py(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function my(){const e=document.getElementById("app");Qo.createRoot(e).render(y.jsx(v.StrictMode,{children:y.jsx(ny,{})}))}document.addEventListener("DOMContentLoaded",my); +*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function ph(e){var t=mh(e,"string");return typeof t=="symbol"?t:String(t)}function mh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function hh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var d=arguments.length,m=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Ah(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Uh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function po(e,t){return y.useMemo(()=>Uh(e,t),[e,t])}function Bh(e){return e&&"setState"in e?Cn.findDOMNode(e):e??null}const Hh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const m=y.useRef(null),p=po(m,s),g=N=>{p(Bh(N))},w=N=>T=>{N&&m.current&&N(m.current,T)},k=y.useCallback(w(e),[e]),L=y.useCallback(w(t),[t]),f=y.useCallback(w(n),[n]),c=y.useCallback(w(r),[r]),h=y.useCallback(w(l),[l]),S=y.useCallback(w(o),[o]),C=y.useCallback(w(i),[i]);return v.jsx(Mh,{ref:d,...a,onEnter:k,onEntered:f,onEntering:L,onExit:c,onExited:S,onExiting:h,addEndListener:C,nodeRef:m,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Wt.cloneElement(u,{ref:g})})}),Wh=Hh;function Vh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Vh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Vf=e=>y.forwardRef((t,n)=>v.jsx("div",{...t,ref:n,className:M(t.className,e)})),Qf=Vf("h4");Qf.displayName="DivStyledAsH4";const Kf=y.forwardRef(({className:e,bsPrefix:t,as:n=Qf,...r},l)=>(t=H(t,"alert-heading"),v.jsx(n,{ref:l,className:M(e,t),...r})));Kf.displayName="AlertHeading";const Qh=Kf;function Kh(){return y.useState(null)}function Gh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Yh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Xh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Zh=typeof document<"u",ca=Zh||Xh?y.useLayoutEffect:y.useEffect,Jh=["as","disabled"];function qh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function bh(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&bh(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},m=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:m},a]}const ev=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=qh(e,Jh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return v.jsx(i,Object.assign({},l,o,{ref:t}))});ev.displayName="Button";const tv=["onKeyDown"];function nv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function rv(e){return!e||e.trim()==="#"}const Gf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=nv(e,tv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return rv(r.href)||r.role==="button"?v.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):v.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Gf.displayName="Anchor";const lv=Gf,Yf=y.forwardRef(({className:e,bsPrefix:t,as:n=lv,...r},l)=>(t=H(t,"alert-link"),v.jsx(n,{ref:l,className:M(e,t),...r})));Yf.displayName="AlertLink";const ov=Yf,iv={[St]:"show",[Ht]:"show"},Xf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Ah(s),r==null||r(s,a)},[r]);return v.jsx(Wh,{ref:o,addEndListener:Ih,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,iv[s],n[s])})})});Xf.displayName="Fade";const Ql=Xf,uv={"aria-label":ot.string,onClick:ot.func,variant:ot.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>v.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=uv;const Zf=Wu,Jf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:m=Ql,...p}=vh(e,{show:"onClose"}),g=H(n,"alert"),w=Pe(f=>{a&&a(!1,f)}),k=m===!0?Ql:m,L=v.jsxs("div",{role:"alert",...k?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&v.jsx(Zf,{onClick:w,"aria-label":l,variant:o}),u]});return k?v.jsx(k,{unmountOnExit:!0,...p,ref:void 0,in:r,children:L}):r?L:null});Jf.displayName="Alert";const fa=Object.assign(Jf,{Link:ov,Heading:Qh}),qf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:m}]=Hu({tagName:e,disabled:o,...u}),p=m;return v.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});qf.displayName="Button";const Mr=qf;function sv(e){const t=y.useRef(e);return t.current=e,t}function bf(e){const t=sv(e);y.useEffect(()=>()=>t.current(),[])}function av(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function cv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function fv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=$f(),o=Df(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,m,p;typeof a=="object"&&a!=null?{span:d,offset:m,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),m!=null&&u.push(`offset${g}-${m}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const ed=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=fv(e);return v.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});ed.displayName="Col";const Vu=ed,td=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return v.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});td.displayName="Container";const dv=td;var pv=Function.prototype.bind.call(Function.prototype.call,[].slice);function cn(e,t){return pv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const mv="data-rr-ui-";function hv(e){return`${mv}${e}`}const nd=y.createContext(Wn?window:void 0);nd.Provider;function Qu(){return y.useContext(nd)}const vv={type:ot.string,tooltip:ot.bool,as:ot.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>v.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=vv;const rd=Ku,yv=y.createContext({}),ct=yv,ld=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ct);return t=H(t,"form-check-input"),v.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});ld.displayName="FormCheckInput";const od=ld,id=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-check-label"),v.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});id.displayName="FormCheckLabel";const Ki=id,ud=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:m,style:p,title:g="",type:w="checkbox",label:k,children:L,as:f="input",...c},h)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ct),C=y.useMemo(()=>({controlId:e||S}),[S,e]),N=!L&&k!=null&&k!==!1||cv(L,Ki),T=v.jsx(od,{...c,type:w==="switch"?"checkbox":w,ref:h,isValid:i,isInvalid:u,disabled:o,as:f});return v.jsx(ct.Provider,{value:C,children:v.jsx("div",{style:p,className:M(m,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,w==="switch"&&n),children:L||v.jsxs(v.Fragment,{children:[T,N&&v.jsx(Ki,{title:g,children:k}),a&&v.jsx(rd,{type:d,tooltip:s,children:a})]})})})});ud.displayName="FormCheck";const Kl=Object.assign(ud,{Input:od,Label:Ki}),sd=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...m},p)=>{const{controlId:g}=y.useContext(ct);return e=H(e,"form-control"),v.jsx(d,{...m,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});sd.displayName="FormControl";const gv=Object.assign(sd,{Feedback:rd}),ad=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),v.jsx(n,{ref:l,className:M(e,t),...r})));ad.displayName="FormFloating";const wv=ad,cd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return v.jsx(ct.Provider,{value:l,children:v.jsx(t,{...n,ref:r})})});cd.displayName="FormGroup";const fd=cd,dd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ct);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?v.jsx(Vu,{ref:u,as:"label",className:d,htmlFor:o,...i}):v.jsx(e,{ref:u,className:d,htmlFor:o,...i})});dd.displayName="FormLabel";const Sv=dd,pd=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-range"),v.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});pd.displayName="FormRange";const kv=pd,md=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ct);return e=H(e,"form-select"),v.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});md.displayName="FormSelect";const Ev=md,hd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),v.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));hd.displayName="FormText";const xv=hd,vd=y.forwardRef((e,t)=>v.jsx(Kl,{...e,ref:t,type:"switch"}));vd.displayName="Switch";const Cv=Object.assign(vd,{Input:Kl.Input,Label:Kl.Label}),yd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),v.jsxs(fd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,v.jsx("label",{htmlFor:r,children:l})]})));yd.displayName="FloatingLabel";const Nv=yd,Tv={_ref:ot.any,validated:ot.bool,as:ot.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>v.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=Tv;const On=Object.assign(Gu,{Group:fd,Control:gv,Floating:wv,Check:Kl,Switch:Cv,Label:Sv,Text:xv,Range:kv,Select:Ev,FloatingLabel:Nv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Wn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Ho(e){e===void 0&&(e=fo());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function _v(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=hv("modal-open");class jv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return _v(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=jv,Wo=(e,t)=>Wn?e==null?(t||fo()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Lv(e,t){const n=Qu(),[r,l]=y.useState(()=>Wo(e,n==null?void 0:n.document));if(!r){const o=Wo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Wo(e);o!==r&&l(o)},[e,r]),r}function Rv({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Pe(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=po(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Ov({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Pe(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Pv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Ov({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,m=>{throw a.in||i(!0),m})}}),s=po(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?v.jsx(e,Object.assign({},n)):t?v.jsx(Pv,Object.assign({},n,{transition:t})):v.jsx(Rv,Object.assign({},n))}function Fv(e){return e.code==="Escape"||e.keyCode===27}const Mv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function zv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Vo;function $v(e){return Vo||(Vo=new Yu({ownerDocument:e==null?void 0:e.document})),Vo}function Dv(e){const t=Qu(),n=e||$v(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const gd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:m,runTransition:p,backdropTransition:g,runBackdropTransition:w,autoFocus:k=!0,enforceFocus:L=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:h,renderBackdrop:S=K=>v.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:j=()=>{},onExit:U,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:rn}=e,Qn=zv(e,Mv);const _e=Qu(),Ke=Lv(N),x=Dv(C),R=Gh(),O=Yh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>x,[x]),Wn&&!O&&n&&(fe.current=Ho(_e==null?void 0:_e.document)),n&&D&&A(!1);const je=Pe(()=>{if(x.add(),on.current=Vl(document,"keydown",mo),ln.current=Vl(document,"focus",()=>setTimeout(Le),!0),T&&T(),k){var K,Hr;const Yn=Ho((K=(Hr=x.dialog)==null?void 0:Hr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Yn&&!da(x.dialog,Yn)&&(fe.current=Yn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),on.current==null||on.current(),ln.current==null||ln.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ke||je()},[n,Ke,je]),y.useEffect(()=>{D&&qe()},[D,qe]),bf(()=>{qe()});const Le=Pe(()=>{if(!L||!R()||!x.isTopModal())return;const K=Ho(_e==null?void 0:_e.document);x.dialog&&K&&!da(x.dialog,K)&&x.dialog.focus()}),mt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),mo=Pe(K=>{s&&Fv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||j())}),ln=y.useRef(),on=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const Br=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=h?h(Br):v.jsx("div",Object.assign({},Br,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(m,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:U,onExiting:ie,onExited:Kn,onEnter:Ve,onEntering:Qe,onEntered:rn,children:Gn});let It=null;return u&&(It=S({ref:x.setBackdropRef,onClick:mt}),It=ha(g,w,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),v.jsx(v.Fragment,{children:Cn.createPortal(v.jsxs(v.Fragment,{children:[It,Gn]}),Ke)})});gd.displayName="Modal";const Iv=Object.assign(gd,{Manager:Yu});function Av(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Uv(e,t){e.classList?e.classList.add(t):Av(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Bv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const fn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Hv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Uv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Bv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Qo;function Wv(e){return Qo||(Qo=new Hv(e)),Qo}const wd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),v.jsx(n,{ref:l,className:M(e,t),...r})));wd.displayName="ModalBody";const Vv=wd,Qv=y.createContext({onHide(){}}),Sd=Qv,kd=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,m=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return v.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&m),children:v.jsx("div",{className:M(`${e}-content`,n),children:i})})});kd.displayName="ModalDialog";const Ed=kd,xd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),v.jsx(n,{ref:l,className:M(e,t),...r})));xd.displayName="ModalFooter";const Kv=xd,Gv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(Sd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return v.jsxs("div",{ref:i,...o,children:[l,n&&v.jsx(Zf,{"aria-label":e,variant:t,onClick:s})]})}),Yv=Gv,Cd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),v.jsx(Yv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Cd.displayName="ModalHeader";const Xv=Cd,Zv=Vf("h4"),Nd=y.forwardRef(({className:e,bsPrefix:t,as:n=Zv,...r},l)=>(t=H(t,"modal-title"),v.jsx(n,{ref:l,className:M(e,t),...r})));Nd.displayName="ModalTitle";const Jv=Nd;function qv(e){return v.jsx(Ql,{...e,timeout:null})}function bv(e){return v.jsx(Ql,{...e,timeout:null})}const Td=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=Ed,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:m=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:w,onShow:k,onHide:L,container:f,autoFocus:c=!0,enforceFocus:h=!0,restoreFocus:S=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:j,onEnter:U,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...rn},Qn)=>{const[_e,Ke]=y.useState({}),[x,R]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,je]=Kh(),qe=po(Qn,je),Le=Pe(L),mt=Sh();e=H(e,"modal");const mo=y.useMemo(()=>({onHide:Le}),[Le]);function ln(){return Qe||Wv({isRTL:mt})}function on($){if(!Wn)return;const un=ln().getScrollbarWidth()>0,Zu=$.scrollHeight>fo($).documentElement.clientHeight;Ke({paddingRight:un&&!Zu?pa():void 0,paddingLeft:!un&&Zu?pa():void 0})}const Kn=Pe(()=>{fe&&on(fe.dialog)});bf(()=>{Qi(window,"resize",Kn),A.current==null||A.current()});const Br=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{R(!0),A.current=Wf(fe.dialog,()=>{R(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}L==null||L()},Yn=$=>{g?w==null||w($):($.preventDefault(),p==="static"&&It())},$d=($,un)=>{$&&on($),U==null||U($,un)},Dd=$=>{A.current==null||A.current(),T==null||T($)},Id=($,un)=>{P==null||P($,un),Hf(window,"resize",Kn)},Ad=$=>{$&&($.style.display=""),ie==null||ie($),Qi(window,"resize",Kn)},Ud=y.useCallback($=>v.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!m&&"show")}),[m,Ve,e]),Xu={...n,..._e};Xu.display="block";const Bd=$=>v.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,x&&`${e}-static`,!m&&"show"),onClick:p?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:v.jsx(i,{...rn,onMouseDown:Br,className:r,contentClassName:l,children:o})});return v.jsx(Sd.Provider,{value:mo,children:v.jsx(Iv,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:h,restoreFocus:S,restoreFocusOptions:C,onEscapeKeyDown:Yn,onShow:k,onHide:L,onEnter:$d,onEntering:Id,onEntered:N,onExit:Dd,onExiting:j,onExited:Ad,manager:ln(),transition:m?qv:void 0,backdropTransition:m?bv:void 0,renderBackdrop:Ud,renderDialog:Bd})})});Td.displayName="Modal";const tt=Object.assign(Td,{Body:Vv,Header:Xv,Title:Jv,Footer:Kv,Dialog:Ed,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ey(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...m},p){return v.jsx("div",{ref:p,...m,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${ey(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?v.jsx("span",{className:"visually-hidden",children:r}):r})}const _d=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p,className:g,children:w,...k}=r;return v.jsx("div",{ref:n,...k,className:M(g,m),children:w?av(w,L=>y.cloneElement(L,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p},n)})});_d.displayName="ProgressBar";const ty=_d,jd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=$f(),u=Df(),s=`${o}-cols`,a=[];return i.forEach(d=>{const m=r[d];delete r[d];let p;m!=null&&typeof m=="object"?{cols:p}=m:p=m;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),v.jsx(n,{ref:l,...r,className:M(t,o,...a)})});jd.displayName="Row";const Ld=jd,Rd=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return v.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Rd.displayName="Spinner";const An=Rd,ny=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",Gi="initializing",ry="paused",Od="live",vt=async(e,t,n)=>{console.log(e,t);const r=ny+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ft={listTorrents:()=>vt("GET","/torrents"),getTorrentDetails:e=>vt("GET",`/torrents/${e}`),getTorrentStats:e=>vt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),vt("POST",n,e)},pause:e=>vt("POST",`/torrents/${e}/pause`),start:e=>vt("POST",`/torrents/${e}/start`),forget:e=>vt("POST",`/torrents/${e}/forget`),delete:e=>vt("POST",`/torrents/${e}/delete`)},Vn=y.createContext(null),Pd=y.createContext(null),Ko=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return v.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},ly=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Vn),d=()=>{l(!1),i(null),s(!1),n()},m=()=>{s(!0),(r?ft.delete:ft.forget)(e).then(()=>{a.refreshTorrents(),d()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return v.jsxs(tt,{show:t,onHide:d,children:[v.jsx(tt.Header,{closeButton:!0,children:"Delete torrent"}),v.jsxs(tt.Body,{children:[v.jsx(On,{children:v.jsx(On.Group,{controlId:"delete-torrent",children:v.jsx(On.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&v.jsx(zr,{error:o})]}),v.jsxs(tt.Footer,{children:[u&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:m,disabled:u,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:d,children:"Cancel"})]})]})},oy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Pd);const s=n=="live",a=n=="paused",d=y.useContext(Vn),m=()=>{l(!0),ft.start(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error starting torrent id=${e}`,details:k})}).finally(()=>l(!1))},p=()=>{l(!0),ft.pause(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error pausing torrent id=${e}`,details:k})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},w=()=>{l(!1),i(!1)};return v.jsx(Ld,{children:v.jsxs(Vu,{children:[a&&v.jsx(Ko,{className:"bi-play-circle",onClick:m,disabled:r,color:"success"}),s&&v.jsx(Ko,{className:"bi-pause-circle",onClick:p,disabled:r}),v.jsx(Ko,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),v.jsx(ly,{id:e,show:o,onHide:w})]})})},iy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Gi||r==Od)&&!u,d=l?"Error":`${s.toFixed(2)}%`,m=l?"danger":u?"success":"primary",p=()=>{var L;let k=(L=n==null?void 0:n.live)==null?void 0:L.snapshot.peer_stats;return k?`${k.live} / ${k.seen}`:""},g=()=>{var k;return u?"Completed":r==Gi?"Checking files":((k=n.live)==null?void 0:k.download_speed.human_readable)??"N/A"};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),v.jsxs(Ld,{className:w.join(" "),children:[v.jsx(yt,{size:3,label:"Name",children:t?v.jsxs(v.Fragment,{children:[v.jsx("div",{className:"text-truncate",children:vy(t)}),l&&v.jsxs("p",{className:"text-danger",children:[v.jsx("strong",{children:"Error:"})," ",l]})]}):v.jsx(An,{})}),n?v.jsxs(v.Fragment,{children:[v.jsx(yt,{label:"Size",children:`${Md(o)} `}),v.jsx(yt,{size:2,label:r==ry?"Progress (PAUSED)":"Progress",children:v.jsx(ty,{now:s,label:d,animated:a,variant:m})}),v.jsx(yt,{size:2,label:"Down Speed",children:g()}),v.jsx(yt,{label:"ETA",children:yy(n)}),v.jsx(yt,{size:2,label:"Peers",children:p()}),v.jsx(yt,{label:"Actions",children:v.jsx(oy,{id:e,statsResponse:n})})]}):v.jsx(yt,{label:"Loading stats",size:8,children:v.jsx(An,{})})]})},yt=({size:e,label:t,children:n})=>v.jsxs(Vu,{md:e||1,className:"py-3",children:[v.jsx("div",{className:"fw-bold",children:t}),n]}),uy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null);y.useEffect(()=>{if(n===null)return wy(async()=>{await ft.getTorrentDetails(t.id).then(r)},1e3)},[n]);const i=()=>ft.getTorrentStats(t.id).then(u=>(o(u),u));return y.useEffect(()=>zd(async()=>i().then(m=>m.finished?1e4:m.state==Gi||m.state==Od?1e3:1e4,m=>1e4),0),[]),v.jsx(Pd.Provider,{value:{refresh:i},children:v.jsx(iy,{id:e,detailsResponse:n,statsResponse:l})})},sy=e=>{if(e.torrents===null&&e.loading)return v.jsx(An,{});if(e.torrents!==null)return e.torrents.length===0?v.jsx("div",{className:"text-center",children:v.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):v.jsx(v.Fragment,{children:e.torrents.map(t=>v.jsx(uy,{id:t.id,torrent:t},t.id))})},ay=()=>{const[e,t]=y.useState(null),[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(!1),s=async()=>{u(!0);let d=await ft.listTorrents().finally(()=>u(!1));o(d.torrents)};y.useEffect(()=>zd(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return v.jsx(Vn.Provider,{value:a,children:v.jsxs("div",{className:"text-center",children:[v.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 0.0.1-alpha"}),v.jsx(hy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},cy=e=>{let{details:t}=e;return t?v.jsxs(v.Fragment,{children:[t.status&&v.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},zr=e=>{let{error:t,remove:n}=e;return t==null?null:v.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[v.jsx(fa.Heading,{children:t.text}),v.jsx(cy,{details:t.details})]})},Fd=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState([]),[a,d]=y.useState(null);y.useContext(Vn);const m=n!==null||a!==null;y.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const w=await ft.uploadTorrent(n,{listOnly:!0});s(w.details.files)}catch(w){d({text:"Error listing torrent",details:w})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return v.jsxs(v.Fragment,{children:[v.jsx(Mr,{variant:l,onClick:t,className:"m-1",children:e}),v.jsx(py,{show:m,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},fy=()=>{let[e,t]=y.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return v.jsx(Fd,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},dy=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return v.jsxs(v.Fragment,{children:[v.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),v.jsx(Fd,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},py=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=y.useState([]),[a,d]=y.useState(!1),[m,p]=y.useState(null),g=y.useContext(Vn);y.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const w=()=>{n(),s([]),p(null),d(!1)},k=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},L=async()=>{d(!0),ft.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return v.jsxs(tt,{show:t,onHide:w,size:"lg",children:[v.jsx(tt.Header,{closeButton:!0,children:!!l||v.jsx(tt.Title,{children:"Select Files"})}),v.jsxs(tt.Body,{children:[o?v.jsx(An,{}):l?v.jsx(zr,{error:l}):v.jsx(On,{children:r.map((f,c)=>v.jsx(On.Group,{controlId:`check-${c}`,children:v.jsx(On.Check,{type:"checkbox",label:`${f.name} (${Md(f.length)})`,checked:u.includes(c),onChange:()=>k(c)})},c))}),v.jsx(zr,{error:m})]}),v.jsxs(tt.Footer,{children:[a&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:L,disabled:o||a||u.length==0,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:w,children:"Cancel"})]})]})},my=()=>v.jsxs("div",{id:"buttons-container",className:"mt-3",children:[v.jsx(fy,{}),v.jsx(dy,{})]}),hy=e=>{let t=y.useContext(Vn);return v.jsxs(dv,{children:[v.jsx(zr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),v.jsx(zr,{error:e.otherError}),v.jsx(sy,{torrents:e.torrents,loading:e.torrentsLoading}),v.jsx(my,{})]})};function Md(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function vy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function yy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":gy(t)}function gy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function zd(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function wy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function Sy(){const e=document.getElementById("app");Go.createRoot(e).render(v.jsx(y.StrictMode,{children:v.jsx(ay,{})}))}document.addEventListener("DOMContentLoaded",Sy); diff --git a/crates/librqbit/webui/dist/index.html b/crates/librqbit/webui/dist/index.html index 3031320..ce34a3d 100644 --- a/crates/librqbit/webui/dist/index.html +++ b/crates/librqbit/webui/dist/index.html @@ -8,6 +8,8 @@ + diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 9e41ea7..5edab1a 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -1,7 +1,7 @@ import { MouseEventHandler, StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner, Table } from 'react-bootstrap'; -import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE } from './api'; +import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED } from './api'; interface Error { text: string, @@ -161,7 +161,7 @@ const TorrentRow: React.FC<{ const progressPercentage = error ? 100 : (progressBytes / totalBytes) * 100; const isAnimated = (state == STATE_INITIALIZING || state == STATE_LIVE) && !finished; const progressLabel = error ? 'Error' : `${progressPercentage.toFixed(2)}%`; - const progressBarVariant = error ? 'danger' : finished ? 'success' : 'info'; + const progressBarVariant = error ? 'danger' : finished ? 'success' : 'primary'; const formatPeersString = () => { let peer_stats = statsResponse?.live?.snapshot.peer_stats; @@ -206,8 +206,12 @@ const TorrentRow: React.FC<{ {statsResponse ? <> {`${formatBytes(totalBytes)} `} - - + + {formatDownloadSped()} {getCompletionETA(statsResponse)} From 79bd41a552289c8c7b2fd43fa9408cfc8565451b Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 01:24:57 +0000 Subject: [PATCH 24/38] Can now reload RUST_LOG at runtime from HTTP API --- crates/librqbit/src/chunk_tracker.rs | 10 +++-- crates/librqbit/src/http_api.rs | 38 +++++++++++++++---- crates/librqbit/src/torrent_state/live/mod.rs | 2 +- crates/rqbit/Cargo.toml | 2 +- crates/rqbit/src/main.rs | 35 ++++++++++++++--- 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/crates/librqbit/src/chunk_tracker.rs b/crates/librqbit/src/chunk_tracker.rs index 008e13e..6f0ec5e 100644 --- a/crates/librqbit/src/chunk_tracker.rs +++ b/crates/librqbit/src/chunk_tracker.rs @@ -1,6 +1,6 @@ use librqbit_core::lengths::{ChunkInfo, Lengths, ValidPieceIndex}; use peer_binary_protocol::Piece; -use tracing::{debug, info}; +use tracing::{debug, trace}; use crate::type_aliases::BF; @@ -129,7 +129,7 @@ impl ChunkTracker { } pub fn mark_piece_broken(&mut self, index: ValidPieceIndex) -> bool { - info!("remarking piece={} as broken", index); + debug!("remarking piece={} as broken", index); self.needed_pieces.set(index.get() as usize, true); self.chunk_status .get_mut(self.lengths.chunk_range(index)) @@ -170,9 +170,11 @@ impl ChunkTracker { return Some(ChunkMarkingResult::PreviouslyCompleted); } chunk_range.set(chunk_info.chunk_index as usize, true); - debug!( + trace!( "piece={}, chunk_info={:?}, bits={:?}", - piece.index, chunk_info, chunk_range, + piece.index, + chunk_info, + chunk_range, ); if chunk_range.all() { diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 0b112ba..4266c6a 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -14,6 +14,7 @@ use std::net::SocketAddr; use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::{Duration, Instant}; +use tokio::sync::mpsc::UnboundedSender; use tracing::{info, warn}; use axum::Router; @@ -33,9 +34,9 @@ pub struct HttpApi { } impl HttpApi { - pub fn new(session: Arc) -> Self { + pub fn new(session: Arc, rust_log_reload_tx: Option>) -> Self { Self { - inner: Arc::new(ApiInternal::new(session)), + inner: Arc::new(ApiInternal::new(session, rust_log_reload_tx)), } } @@ -51,11 +52,14 @@ impl HttpApi { "GET /torrents": "List torrents (default torrent is 0)", "GET /torrents/{index}": "Torrent details", "GET /torrents/{index}/haves": "The bitfield of have pieces", - "GET /torrents/{index}/stats": "Torrent stats", + "GET /torrents/{index}/stats/v1": "Torrent stats", "GET /torrents/{index}/peer_stats": "Per peer stats", - // This is kind of not secure as it just reads any local file that it has access to, - // or any URL, but whatever, ok for our purposes / threat model. + "POST /torrents/{index}/pause": "Pause torrent", + "POST /torrents/{index}/start": "Resume torrent", + "POST /torrents/{index}/forget": "Forget about the torrent, keep the files", + "POST /torrents/{index}/delete": "Forget about the torrent, remove the files", "POST /torrents": "Add a torrent here. magnet: or http:// or a local file.", + "POST /rust_log": "Set RUST_LOG to this post launch (for debugging)", "GET /web/": "Web UI", }, "server": "rqbit", @@ -151,6 +155,13 @@ impl HttpApi { state.api_torrent_action_delete(idx).map(axum::Json) } + async fn set_rust_log( + State(state): State, + new_value: String, + ) -> Result { + state.api_set_rust_log(new_value).map(axum::Json) + } + #[allow(unused_mut)] let mut app = Router::new() .route("/", get(api_root)) @@ -165,7 +176,8 @@ impl HttpApi { .route("/torrents/:id/pause", post(torrent_action_pause)) .route("/torrents/:id/start", post(torrent_action_start)) .route("/torrents/:id/forget", post(torrent_action_forget)) - .route("/torrents/:id/delete", post(torrent_action_delete)); + .route("/torrents/:id/delete", post(torrent_action_delete)) + .route("/rust_log", post(set_rust_log)); #[cfg(feature = "webui")] { @@ -383,15 +395,17 @@ impl TorrentAddQueryParams { struct ApiInternal { startup_time: Instant, session: Arc, + rust_log_reload_tx: Option>, } type ApiState = Arc; impl ApiInternal { - pub fn new(session: Arc) -> Self { + pub fn new(session: Arc, rust_log_reload_tx: Option>) -> Self { Self { startup_time: Instant::now(), session, + rust_log_reload_tx, } } @@ -460,6 +474,16 @@ impl ApiInternal { Ok(Default::default()) } + fn api_set_rust_log(&self, new_value: String) -> Result { + let tx = self + .rust_log_reload_tx + .as_ref() + .context("rust_log_reload_tx was not set")?; + tx.send(new_value) + .context("noone is listening to RUST_LOG changes")?; + Ok(Default::default()) + } + pub async fn api_add_torrent( &self, add: AddTorrent<'_>, diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 68f035d..4045d85 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -730,7 +730,7 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler { } Message::Have(h) => self.on_have(h), Message::NotInterested => { - info!("received \"not interested\", but we don't care yet") + debug!("received \"not interested\", but we don't care yet") } message => { warn!("received unsupported message {:?}, ignoring", message); diff --git a/crates/rqbit/Cargo.toml b/crates/rqbit/Cargo.toml index 4e73d12..a0c5410 100644 --- a/crates/rqbit/Cargo.toml +++ b/crates/rqbit/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" [features] default = ["sha1-system", "default-tls", "webui"] -tokio-console = ["console-subscriber"] +tokio-console = ["console-subscriber",] webui = ["librqbit/webui"] timed_existence = ["librqbit/timed_existence"] sha1-system = ["librqbit/sha1-system"] diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 87becb3..4e79124 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -134,7 +134,8 @@ enum SubCommand { Download(DownloadOpts), } -fn init_logging(opts: &Opts) { +// Iint logging and make a channel to send new RUST_LOG values to. +fn init_logging(opts: &Opts) -> tokio::sync::mpsc::UnboundedSender { let default_rust_log = match opts.log_level.as_ref() { Some(level) => match level { LogLevel::Trace => "trace", @@ -147,13 +148,16 @@ fn init_logging(opts: &Opts) { }; let stderr_filter = match std::env::var("RUST_LOG").ok() { Some(rust_log) => EnvFilter::builder() - .parse(&rust_log) + .parse(rust_log) .expect("can't parse RUST_LOG"), None => EnvFilter::builder() .parse(default_rust_log) .expect("can't parse default_rust_log"), }; + let (stderr_filter, reload_stderr_filter) = + tracing_subscriber::reload::Layer::new(stderr_filter); + use tracing_subscriber::{fmt, prelude::*, EnvFilter}; #[cfg(feature = "tokio-console")] @@ -187,6 +191,27 @@ fn init_logging(opts: &Opts) { .with(stderr_filter) .init(); } + + let (reload_tx, mut reload_rx) = tokio::sync::mpsc::unbounded_channel::(); + spawn( + "fmt_filter_reloader", + error_span!("fmt_filter_reloader"), + async move { + while let Some(rust_log) = reload_rx.recv().await { + let stderr_env_filter = match EnvFilter::builder().parse(&rust_log) { + Ok(f) => f, + Err(e) => { + eprintln!("can't parse env filter {:?}: {:#?}", rust_log, e); + continue; + } + }; + eprintln!("setting RUST_LOG to {:?}", rust_log); + let _ = reload_stderr_filter.reload(stderr_env_filter); + } + Ok(()) + }, + ); + reload_tx } fn _start_deadlock_detector_thread() { @@ -248,7 +273,7 @@ fn main() -> anyhow::Result<()> { } async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> { - init_logging(&opts); + let logging_reload_tx = init_logging(&opts); let sopts = SessionOptions { disable_dht: opts.disable_dht, @@ -331,7 +356,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> trace_span!("stats_printer"), stats_printer(session.clone()), ); - let http_api = HttpApi::new(session); + let http_api = HttpApi::new(session, Some(logging_reload_tx)); let http_api_listen_addr = opts.http_api_listen_addr; http_api .make_http_api_and_run(http_api_listen_addr) @@ -411,7 +436,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> trace_span!("stats_printer"), stats_printer(session.clone()), ); - let http_api = HttpApi::new(session.clone()); + let http_api = HttpApi::new(session.clone(), Some(logging_reload_tx)); let http_api_listen_addr = opts.http_api_listen_addr; spawn( "http_api", From e467787c384ea73708356ba74b29b57dd158e1ef Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 01:29:20 +0000 Subject: [PATCH 25/38] Force stats refresh works better now on pause/unpause --- crates/librqbit/webui/dist/app.js | 12 ++++++------ crates/librqbit/webui/src/index.tsx | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/crates/librqbit/webui/dist/app.js b/crates/librqbit/webui/dist/app.js index 06595b2..6d7fa13 100644 --- a/crates/librqbit/webui/dist/app.js +++ b/crates/librqbit/webui/dist/app.js @@ -14,7 +14,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var rp=y,lp=Symbol.for("react.element"),op=Symbol.for("react.fragment"),ip=Object.prototype.hasOwnProperty,up=rp.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,sp={key:!0,ref:!0,__self:!0,__source:!0};function ja(e,t,n){var r,l={},o=null,i=null;n!==void 0&&(o=""+n),t.key!==void 0&&(o=""+t.key),t.ref!==void 0&&(i=t.ref);for(r in t)ip.call(t,r)&&!sp.hasOwnProperty(r)&&(l[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps,t)l[r]===void 0&&(l[r]=t[r]);return{$$typeof:lp,type:e,key:o,ref:i,props:l,_owner:up.current}}Yl.Fragment=op;Yl.jsx=ja;Yl.jsxs=ja;wa.exports=Yl;var v=wa.exports,Go={},La={exports:{}},Ne={},Ra={exports:{}},Oa={};/** + */var rp=y,lp=Symbol.for("react.element"),op=Symbol.for("react.fragment"),ip=Object.prototype.hasOwnProperty,up=rp.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,sp={key:!0,ref:!0,__self:!0,__source:!0};function ja(e,t,n){var r,l={},o=null,i=null;n!==void 0&&(o=""+n),t.key!==void 0&&(o=""+t.key),t.ref!==void 0&&(i=t.ref);for(r in t)ip.call(t,r)&&!sp.hasOwnProperty(r)&&(l[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps,t)l[r]===void 0&&(l[r]=t[r]);return{$$typeof:lp,type:e,key:o,ref:i,props:l,_owner:up.current}}Yl.Fragment=op;Yl.jsx=ja;Yl.jsxs=ja;wa.exports=Yl;var v=wa.exports,Go={},Ra={exports:{}},Ne={},La={exports:{}},Oa={};/** * @license React * scheduler.production.min.js * @@ -22,7 +22,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */(function(e){function t(x,R){var O=x.length;x.push(R);e:for(;0>>1,A=x[D];if(0>>1;Dl(qe,O))Lel(mt,qe)?(x[D]=mt,x[Le]=O,D=Le):(x[D]=qe,x[je]=O,D=je);else if(Lel(mt,O))x[D]=mt,x[Le]=O,D=Le;else break e}}return R}function l(x,R){var O=x.sortIndex-R.sortIndex;return O!==0?O:x.id-R.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],a=[],d=1,m=null,p=3,g=!1,w=!1,k=!1,L=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function h(x){for(var R=n(a);R!==null;){if(R.callback===null)r(a);else if(R.startTime<=x)r(a),R.sortIndex=R.expirationTime,t(s,R);else break;R=n(a)}}function S(x){if(k=!1,h(x),!w)if(n(s)!==null)w=!0,_e(C);else{var R=n(a);R!==null&&Ke(S,R.startTime-x)}}function C(x,R){w=!1,k&&(k=!1,f(j),j=-1),g=!0;var O=p;try{for(h(R),m=n(s);m!==null&&(!(m.expirationTime>R)||x&&!ie());){var D=m.callback;if(typeof D=="function"){m.callback=null,p=m.priorityLevel;var A=D(m.expirationTime<=R);R=e.unstable_now(),typeof A=="function"?m.callback=A:m===n(s)&&r(s),h(R)}else r(s);m=n(s)}if(m!==null)var fe=!0;else{var je=n(a);je!==null&&Ke(S,je.startTime-R),fe=!1}return fe}finally{m=null,p=O,g=!1}}var N=!1,T=null,j=-1,U=5,P=-1;function ie(){return!(e.unstable_now()-Px||125D?(x.sortIndex=O,t(a,x),n(s)===null&&x===n(a)&&(k?(f(j),j=-1):k=!0,Ke(S,O-D))):(x.sortIndex=A,t(s,x),w||g||(w=!0,_e(C))),x},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(x){var R=p;return function(){var O=p;p=R;try{return x.apply(this,arguments)}finally{p=O}}}})(Oa);Ra.exports=Oa;var ap=Ra.exports;/** + */(function(e){function t(x,L){var O=x.length;x.push(L);e:for(;0>>1,A=x[D];if(0>>1;Dl(qe,O))Rel(mt,qe)?(x[D]=mt,x[Re]=O,D=Re):(x[D]=qe,x[je]=O,D=je);else if(Rel(mt,O))x[D]=mt,x[Re]=O,D=Re;else break e}}return L}function l(x,L){var O=x.sortIndex-L.sortIndex;return O!==0?O:x.id-L.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],a=[],d=1,m=null,p=3,g=!1,w=!1,k=!1,R=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function h(x){for(var L=n(a);L!==null;){if(L.callback===null)r(a);else if(L.startTime<=x)r(a),L.sortIndex=L.expirationTime,t(s,L);else break;L=n(a)}}function S(x){if(k=!1,h(x),!w)if(n(s)!==null)w=!0,_e(C);else{var L=n(a);L!==null&&Ke(S,L.startTime-x)}}function C(x,L){w=!1,k&&(k=!1,f(j),j=-1),g=!0;var O=p;try{for(h(L),m=n(s);m!==null&&(!(m.expirationTime>L)||x&&!ie());){var D=m.callback;if(typeof D=="function"){m.callback=null,p=m.priorityLevel;var A=D(m.expirationTime<=L);L=e.unstable_now(),typeof A=="function"?m.callback=A:m===n(s)&&r(s),h(L)}else r(s);m=n(s)}if(m!==null)var fe=!0;else{var je=n(a);je!==null&&Ke(S,je.startTime-L),fe=!1}return fe}finally{m=null,p=O,g=!1}}var N=!1,T=null,j=-1,U=5,P=-1;function ie(){return!(e.unstable_now()-Px||125D?(x.sortIndex=O,t(a,x),n(s)===null&&x===n(a)&&(k?(f(j),j=-1):k=!0,Ke(S,O-D))):(x.sortIndex=A,t(s,x),w||g||(w=!0,_e(C))),x},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(x){var L=p;return function(){var O=p;p=L;try{return x.apply(this,arguments)}finally{p=O}}}})(Oa);La.exports=Oa;var ap=La.exports;/** * @license React * react-dom.production.min.js * @@ -34,11 +34,11 @@ `+vo+e}var yo=!1;function go(e,t){if(!e||yo)return"";yo=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(t,[])}catch(a){var r=a}Reflect.construct(e,[],t)}else{try{t.call()}catch(a){r=a}e.call(t.prototype)}else{try{throw Error()}catch(a){r=a}e()}}catch(a){if(a&&r&&typeof a.stack=="string"){for(var l=a.stack.split(` `),o=r.stack.split(` `),i=l.length-1,u=o.length-1;1<=i&&0<=u&&l[i]!==o[u];)u--;for(;1<=i&&0<=u;i--,u--)if(l[i]!==o[u]){if(i!==1||u!==1)do if(i--,u--,0>u||l[i]!==o[u]){var s=` -`+l[i].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{yo=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?rr(e):""}function mp(e){switch(e.tag){case 5:return rr(e.type);case 16:return rr("Lazy");case 13:return rr("Suspense");case 19:return rr("SuspenseList");case 0:case 2:case 15:return e=go(e.type,!1),e;case 11:return e=go(e.type.render,!1),e;case 1:return e=go(e.type,!0),e;default:return""}}function qo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case pn:return"Fragment";case dn:return"Portal";case Xo:return"Profiler";case tu:return"StrictMode";case Zo:return"Suspense";case Jo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case za:return(e.displayName||"Context")+".Consumer";case Ma:return(e._context.displayName||"Context")+".Provider";case nu:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ru:return t=e.displayName||null,t!==null?t:qo(e.type)||"Memo";case gt:t=e._payload,e=e._init;try{return qo(e(t))}catch{}}return null}function hp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return qo(t);case 8:return t===tu?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Ft(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Da(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function vp(e){var t=Da(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Qr(e){e._valueTracker||(e._valueTracker=vp(e))}function Ia(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Da(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Sl(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function bo(e,t){var n=t.checked;return X({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function rs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Ft(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Aa(e,t){t=t.checked,t!=null&&eu(e,"checked",t,!1)}function ei(e,t){Aa(e,t);var n=Ft(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ti(e,t.type,n):t.hasOwnProperty("defaultValue")&&ti(e,t.type,Ft(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ls(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ti(e,t,n){(t!=="number"||Sl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var lr=Array.isArray;function Nn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Kr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function gr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var sr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},yp=["Webkit","ms","Moz","O"];Object.keys(sr).forEach(function(e){yp.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),sr[t]=sr[e]})});function Wa(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||sr.hasOwnProperty(e)&&sr[e]?(""+t).trim():t+"px"}function Va(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Wa(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var gp=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function li(e,t){if(t){if(gp[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(E(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(E(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(E(61))}if(t.style!=null&&typeof t.style!="object")throw Error(E(62))}}function oi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ii=null;function lu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ui=null,Tn=null,_n=null;function us(e){if(e=Ar(e)){if(typeof ui!="function")throw Error(E(280));var t=e.stateNode;t&&(t=bl(t),ui(e.stateNode,e.type,t))}}function Qa(e){Tn?_n?_n.push(e):_n=[e]:Tn=e}function Ka(){if(Tn){var e=Tn,t=_n;if(_n=Tn=null,us(e),t)for(e=0;e>>=0,e===0?32:31-(Lp(e)/Rp|0)|0}var Gr=64,Yr=4194304;function or(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Cl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=or(u):(o&=i,o!==0&&(r=or(o)))}else i=n&~l,i!==0?r=or(i):o!==0&&(r=or(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Dr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Be(t),e[t]=n}function Mp(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=cr),vs=String.fromCharCode(32),ys=!1;function dc(e,t){switch(e){case"keyup":return sm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function pc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var mn=!1;function cm(e,t){switch(e){case"compositionend":return pc(t);case"keypress":return t.which!==32?null:(ys=!0,vs);case"textInput":return e=t.data,e===vs&&ys?null:e;default:return null}}function fm(e,t){if(mn)return e==="compositionend"||!du&&dc(e,t)?(e=cc(),fl=au=xt=null,mn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ks(n)}}function yc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?yc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function gc(){for(var e=window,t=Sl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Sl(e.document)}return t}function pu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Sm(e){var t=gc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&yc(n.ownerDocument.documentElement,n)){if(r!==null&&pu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Es(n,o);var i=Es(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,hn=null,pi=null,dr=null,mi=!1;function xs(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;mi||hn==null||hn!==Sl(r)||(r=hn,"selectionStart"in r&&pu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),dr&&Cr(dr,r)||(dr=r,r=_l(pi,"onSelect"),0gn||(e.current=Si[gn],Si[gn]=null,gn--)}function B(e,t){gn++,Si[gn]=e.current,e.current=t}var Mt={},ce=$t(Mt),ge=$t(!1),Zt=Mt;function Fn(e,t){var n=e.type.contextTypes;if(!n)return Mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function we(e){return e=e.childContextTypes,e!=null}function Ll(){V(ge),V(ce)}function Rs(e,t,n){if(ce.current!==Mt)throw Error(E(168));B(ce,t),B(ge,n)}function _c(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(E(108,hp(e)||"Unknown",l));return X({},n,r)}function Rl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Mt,Zt=ce.current,B(ce,e),B(ge,ge.current),!0}function Os(e,t,n){var r=e.stateNode;if(!r)throw Error(E(169));n?(e=_c(e,t,Zt),r.__reactInternalMemoizedMergedChildContext=e,V(ge),V(ce),B(ce,e)):V(ge),B(ge,n)}var et=null,eo=!1,Po=!1;function jc(e){et===null?et=[e]:et.push(e)}function Pm(e){eo=!0,jc(e)}function Dt(){if(!Po&&et!==null){Po=!0;var e=0,t=I;try{var n=et;for(I=1;e>=i,l-=i,nt=1<<32-Be(t)+l|n<j?(U=T,T=null):U=T.sibling;var P=p(f,T,h[j],S);if(P===null){T===null&&(T=U);break}e&&T&&P.alternate===null&&t(f,T),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P,T=U}if(j===h.length)return n(f,T),Q&&At(f,j),C;if(T===null){for(;jj?(U=T,T=null):U=T.sibling;var ie=p(f,T,P.value,S);if(ie===null){T===null&&(T=U);break}e&&T&&ie.alternate===null&&t(f,T),c=o(ie,c,j),N===null?C=ie:N.sibling=ie,N=ie,T=U}if(P.done)return n(f,T),Q&&At(f,j),C;if(T===null){for(;!P.done;j++,P=h.next())P=m(f,P.value,S),P!==null&&(c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return Q&&At(f,j),C}for(T=r(f,T);!P.done;j++,P=h.next())P=g(T,f,j,P.value,S),P!==null&&(e&&P.alternate!==null&&T.delete(P.key===null?j:P.key),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return e&&T.forEach(function(Ve){return t(f,Ve)}),Q&&At(f,j),C}function L(f,c,h,S){if(typeof h=="object"&&h!==null&&h.type===pn&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Vr:e:{for(var C=h.key,N=c;N!==null;){if(N.key===C){if(C=h.type,C===pn){if(N.tag===7){n(f,N.sibling),c=l(N,h.props.children),c.return=f,f=c;break e}}else if(N.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===gt&&Is(C)===N.type){n(f,N.sibling),c=l(N,h.props),c.ref=er(f,N,h),c.return=f,f=c;break e}n(f,N);break}else t(f,N);N=N.sibling}h.type===pn?(c=Yt(h.props.children,f.mode,S,h.key),c.return=f,f=c):(S=wl(h.type,h.key,h.props,null,f.mode,S),S.ref=er(f,c,h),S.return=f,f=S)}return i(f);case dn:e:{for(N=h.key;c!==null;){if(c.key===N)if(c.tag===4&&c.stateNode.containerInfo===h.containerInfo&&c.stateNode.implementation===h.implementation){n(f,c.sibling),c=l(c,h.children||[]),c.return=f,f=c;break e}else{n(f,c);break}else t(f,c);c=c.sibling}c=Uo(h,f.mode,S),c.return=f,f=c}return i(f);case gt:return N=h._init,L(f,c,N(h._payload),S)}if(lr(h))return w(f,c,h,S);if(Xn(h))return k(f,c,h,S);tl(f,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,c!==null&&c.tag===6?(n(f,c.sibling),c=l(c,h),c.return=f,f=c):(n(f,c),c=Ao(h,f.mode,S),c.return=f,f=c),i(f)):n(f,c)}return L}var zn=$c(!0),Dc=$c(!1),Ur={},Je=$t(Ur),jr=$t(Ur),Lr=$t(Ur);function Kt(e){if(e===Ur)throw Error(E(174));return e}function Eu(e,t){switch(B(Lr,t),B(jr,e),B(Je,Ur),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:ri(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=ri(t,e)}V(Je),B(Je,t)}function $n(){V(Je),V(jr),V(Lr)}function Ic(e){Kt(Lr.current);var t=Kt(Je.current),n=ri(t,e.type);t!==n&&(B(jr,e),B(Je,n))}function xu(e){jr.current===e&&(V(Je),V(jr))}var G=$t(0);function $l(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var Fo=[];function Cu(){for(var e=0;en?n:4,e(!0);var r=Mo.transition;Mo.transition={};try{e(!1),t()}finally{I=n,Mo.transition=r}}function ef(){return $e().memoizedState}function $m(e,t,n){var r=Ot(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},tf(e))nf(t,n);else if(n=Pc(e,t,n,r),n!==null){var l=pe();He(n,e,r,l),rf(n,t,r)}}function Dm(e,t,n){var r=Ot(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(tf(e))nf(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,We(u,i)){var s=t.interleaved;s===null?(l.next=l,Su(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Pc(e,t,l,r),n!==null&&(l=pe(),He(n,e,r,l),rf(n,t,r))}}function tf(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function nf(e,t){pr=Dl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function rf(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,iu(e,n)}}var Il={readContext:ze,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},Im={readContext:ze,useCallback:function(e,t){return Ye().memoizedState=[e,t===void 0?null:t],e},useContext:ze,useEffect:Us,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,hl(4194308,4,Xc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return hl(4194308,4,e,t)},useInsertionEffect:function(e,t){return hl(4,2,e,t)},useMemo:function(e,t){var n=Ye();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ye();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=$m.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ye();return e={current:e},t.memoizedState=e},useState:As,useDebugValue:Lu,useDeferredValue:function(e){return Ye().memoizedState=e},useTransition:function(){var e=As(!1),t=e[0];return e=zm.bind(null,e[1]),Ye().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,l=Ye();if(Q){if(n===void 0)throw Error(E(407));n=n()}else{if(n=t(),ne===null)throw Error(E(349));qt&30||Bc(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Us(Wc.bind(null,r,o,e),[e]),r.flags|=2048,Pr(9,Hc.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ye(),t=ne.identifierPrefix;if(Q){var n=rt,r=nt;n=(r&~(1<<32-Be(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Rr++,0")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{yo=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?rr(e):""}function mp(e){switch(e.tag){case 5:return rr(e.type);case 16:return rr("Lazy");case 13:return rr("Suspense");case 19:return rr("SuspenseList");case 0:case 2:case 15:return e=go(e.type,!1),e;case 11:return e=go(e.type.render,!1),e;case 1:return e=go(e.type,!0),e;default:return""}}function qo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case pn:return"Fragment";case dn:return"Portal";case Xo:return"Profiler";case tu:return"StrictMode";case Zo:return"Suspense";case Jo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case za:return(e.displayName||"Context")+".Consumer";case Ma:return(e._context.displayName||"Context")+".Provider";case nu:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ru:return t=e.displayName||null,t!==null?t:qo(e.type)||"Memo";case gt:t=e._payload,e=e._init;try{return qo(e(t))}catch{}}return null}function hp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return qo(t);case 8:return t===tu?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Ft(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Da(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function vp(e){var t=Da(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Qr(e){e._valueTracker||(e._valueTracker=vp(e))}function Ia(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Da(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Sl(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function bo(e,t){var n=t.checked;return X({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function rs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Ft(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Aa(e,t){t=t.checked,t!=null&&eu(e,"checked",t,!1)}function ei(e,t){Aa(e,t);var n=Ft(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ti(e,t.type,n):t.hasOwnProperty("defaultValue")&&ti(e,t.type,Ft(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ls(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ti(e,t,n){(t!=="number"||Sl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var lr=Array.isArray;function Nn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Kr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function gr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var sr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},yp=["Webkit","ms","Moz","O"];Object.keys(sr).forEach(function(e){yp.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),sr[t]=sr[e]})});function Wa(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||sr.hasOwnProperty(e)&&sr[e]?(""+t).trim():t+"px"}function Va(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Wa(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var gp=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function li(e,t){if(t){if(gp[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(E(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(E(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(E(61))}if(t.style!=null&&typeof t.style!="object")throw Error(E(62))}}function oi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ii=null;function lu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ui=null,Tn=null,_n=null;function us(e){if(e=Ar(e)){if(typeof ui!="function")throw Error(E(280));var t=e.stateNode;t&&(t=bl(t),ui(e.stateNode,e.type,t))}}function Qa(e){Tn?_n?_n.push(e):_n=[e]:Tn=e}function Ka(){if(Tn){var e=Tn,t=_n;if(_n=Tn=null,us(e),t)for(e=0;e>>=0,e===0?32:31-(Rp(e)/Lp|0)|0}var Gr=64,Yr=4194304;function or(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Cl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=or(u):(o&=i,o!==0&&(r=or(o)))}else i=n&~l,i!==0?r=or(i):o!==0&&(r=or(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Dr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Be(t),e[t]=n}function Mp(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=cr),vs=String.fromCharCode(32),ys=!1;function dc(e,t){switch(e){case"keyup":return sm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function pc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var mn=!1;function cm(e,t){switch(e){case"compositionend":return pc(t);case"keypress":return t.which!==32?null:(ys=!0,vs);case"textInput":return e=t.data,e===vs&&ys?null:e;default:return null}}function fm(e,t){if(mn)return e==="compositionend"||!du&&dc(e,t)?(e=cc(),fl=au=xt=null,mn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ks(n)}}function yc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?yc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function gc(){for(var e=window,t=Sl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Sl(e.document)}return t}function pu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Sm(e){var t=gc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&yc(n.ownerDocument.documentElement,n)){if(r!==null&&pu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Es(n,o);var i=Es(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,hn=null,pi=null,dr=null,mi=!1;function xs(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;mi||hn==null||hn!==Sl(r)||(r=hn,"selectionStart"in r&&pu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),dr&&Cr(dr,r)||(dr=r,r=_l(pi,"onSelect"),0gn||(e.current=Si[gn],Si[gn]=null,gn--)}function B(e,t){gn++,Si[gn]=e.current,e.current=t}var Mt={},ce=$t(Mt),ge=$t(!1),Zt=Mt;function Fn(e,t){var n=e.type.contextTypes;if(!n)return Mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function we(e){return e=e.childContextTypes,e!=null}function Rl(){V(ge),V(ce)}function Ls(e,t,n){if(ce.current!==Mt)throw Error(E(168));B(ce,t),B(ge,n)}function _c(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(E(108,hp(e)||"Unknown",l));return X({},n,r)}function Ll(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Mt,Zt=ce.current,B(ce,e),B(ge,ge.current),!0}function Os(e,t,n){var r=e.stateNode;if(!r)throw Error(E(169));n?(e=_c(e,t,Zt),r.__reactInternalMemoizedMergedChildContext=e,V(ge),V(ce),B(ce,e)):V(ge),B(ge,n)}var et=null,eo=!1,Po=!1;function jc(e){et===null?et=[e]:et.push(e)}function Pm(e){eo=!0,jc(e)}function Dt(){if(!Po&&et!==null){Po=!0;var e=0,t=I;try{var n=et;for(I=1;e>=i,l-=i,nt=1<<32-Be(t)+l|n<j?(U=T,T=null):U=T.sibling;var P=p(f,T,h[j],S);if(P===null){T===null&&(T=U);break}e&&T&&P.alternate===null&&t(f,T),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P,T=U}if(j===h.length)return n(f,T),Q&&At(f,j),C;if(T===null){for(;jj?(U=T,T=null):U=T.sibling;var ie=p(f,T,P.value,S);if(ie===null){T===null&&(T=U);break}e&&T&&ie.alternate===null&&t(f,T),c=o(ie,c,j),N===null?C=ie:N.sibling=ie,N=ie,T=U}if(P.done)return n(f,T),Q&&At(f,j),C;if(T===null){for(;!P.done;j++,P=h.next())P=m(f,P.value,S),P!==null&&(c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return Q&&At(f,j),C}for(T=r(f,T);!P.done;j++,P=h.next())P=g(T,f,j,P.value,S),P!==null&&(e&&P.alternate!==null&&T.delete(P.key===null?j:P.key),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return e&&T.forEach(function(Ve){return t(f,Ve)}),Q&&At(f,j),C}function R(f,c,h,S){if(typeof h=="object"&&h!==null&&h.type===pn&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Vr:e:{for(var C=h.key,N=c;N!==null;){if(N.key===C){if(C=h.type,C===pn){if(N.tag===7){n(f,N.sibling),c=l(N,h.props.children),c.return=f,f=c;break e}}else if(N.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===gt&&Is(C)===N.type){n(f,N.sibling),c=l(N,h.props),c.ref=er(f,N,h),c.return=f,f=c;break e}n(f,N);break}else t(f,N);N=N.sibling}h.type===pn?(c=Yt(h.props.children,f.mode,S,h.key),c.return=f,f=c):(S=wl(h.type,h.key,h.props,null,f.mode,S),S.ref=er(f,c,h),S.return=f,f=S)}return i(f);case dn:e:{for(N=h.key;c!==null;){if(c.key===N)if(c.tag===4&&c.stateNode.containerInfo===h.containerInfo&&c.stateNode.implementation===h.implementation){n(f,c.sibling),c=l(c,h.children||[]),c.return=f,f=c;break e}else{n(f,c);break}else t(f,c);c=c.sibling}c=Uo(h,f.mode,S),c.return=f,f=c}return i(f);case gt:return N=h._init,R(f,c,N(h._payload),S)}if(lr(h))return w(f,c,h,S);if(Xn(h))return k(f,c,h,S);tl(f,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,c!==null&&c.tag===6?(n(f,c.sibling),c=l(c,h),c.return=f,f=c):(n(f,c),c=Ao(h,f.mode,S),c.return=f,f=c),i(f)):n(f,c)}return R}var zn=$c(!0),Dc=$c(!1),Ur={},Je=$t(Ur),jr=$t(Ur),Rr=$t(Ur);function Kt(e){if(e===Ur)throw Error(E(174));return e}function Eu(e,t){switch(B(Rr,t),B(jr,e),B(Je,Ur),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:ri(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=ri(t,e)}V(Je),B(Je,t)}function $n(){V(Je),V(jr),V(Rr)}function Ic(e){Kt(Rr.current);var t=Kt(Je.current),n=ri(t,e.type);t!==n&&(B(jr,e),B(Je,n))}function xu(e){jr.current===e&&(V(Je),V(jr))}var G=$t(0);function $l(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var Fo=[];function Cu(){for(var e=0;en?n:4,e(!0);var r=Mo.transition;Mo.transition={};try{e(!1),t()}finally{I=n,Mo.transition=r}}function ef(){return $e().memoizedState}function $m(e,t,n){var r=Ot(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},tf(e))nf(t,n);else if(n=Pc(e,t,n,r),n!==null){var l=pe();He(n,e,r,l),rf(n,t,r)}}function Dm(e,t,n){var r=Ot(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(tf(e))nf(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,We(u,i)){var s=t.interleaved;s===null?(l.next=l,Su(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Pc(e,t,l,r),n!==null&&(l=pe(),He(n,e,r,l),rf(n,t,r))}}function tf(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function nf(e,t){pr=Dl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function rf(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,iu(e,n)}}var Il={readContext:ze,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},Im={readContext:ze,useCallback:function(e,t){return Ye().memoizedState=[e,t===void 0?null:t],e},useContext:ze,useEffect:Us,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,hl(4194308,4,Xc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return hl(4194308,4,e,t)},useInsertionEffect:function(e,t){return hl(4,2,e,t)},useMemo:function(e,t){var n=Ye();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ye();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=$m.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ye();return e={current:e},t.memoizedState=e},useState:As,useDebugValue:Ru,useDeferredValue:function(e){return Ye().memoizedState=e},useTransition:function(){var e=As(!1),t=e[0];return e=zm.bind(null,e[1]),Ye().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,l=Ye();if(Q){if(n===void 0)throw Error(E(407));n=n()}else{if(n=t(),ne===null)throw Error(E(349));qt&30||Bc(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Us(Wc.bind(null,r,o,e),[e]),r.flags|=2048,Pr(9,Hc.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ye(),t=ne.identifierPrefix;if(Q){var n=rt,r=nt;n=(r&~(1<<32-Be(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Lr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Xe]=t,e[_r]=r,pf(e,t,!1,!1),t.stateNode=e;e:{switch(i=oi(n,r),n){case"dialog":W("cancel",e),W("close",e),l=r;break;case"iframe":case"object":case"embed":W("load",e),l=r;break;case"video":case"audio":for(l=0;lIn&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304)}else{if(!r)if(e=$l(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),tr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!Q)return se(t),null}else 2*J()-o.renderingStartTime>In&&n!==1073741824&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=J(),t.sibling=null,n=G.current,B(G,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return zu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ke&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(E(156,t.tag))}function Km(e,t){switch(hu(t),t.tag){case 1:return we(t.type)&&Ll(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return $n(),V(ge),V(ce),Cu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return xu(t),null;case 13:if(V(G),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(E(340));Mn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(G),null;case 4:return $n(),null;case 10:return wu(t.type._context),null;case 22:case 23:return zu(),null;case 24:return null;default:return null}}var rl=!1,ae=!1,Gm=typeof WeakSet=="function"?WeakSet:Set,_=null;function En(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Z(e,t,r)}else n.current=null}function Pi(e,t,n){try{n()}catch(r){Z(e,t,r)}}var Xs=!1;function Ym(e,t){if(hi=Nl,e=gc(),pu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,a=0,d=0,m=e,p=null;t:for(;;){for(var g;m!==n||l!==0&&m.nodeType!==3||(u=i+l),m!==o||r!==0&&m.nodeType!==3||(s=i+r),m.nodeType===3&&(i+=m.nodeValue.length),(g=m.firstChild)!==null;)p=m,m=g;for(;;){if(m===e)break t;if(p===n&&++a===l&&(u=i),p===o&&++d===r&&(s=i),(g=m.nextSibling)!==null)break;m=p,p=m.parentNode}m=g}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(vi={focusedElem:e,selectionRange:n},Nl=!1,_=t;_!==null;)if(t=_,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_=e;else for(;_!==null;){t=_;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var k=w.memoizedProps,L=w.memoizedState,f=t.stateNode,c=f.getSnapshotBeforeUpdate(t.elementType===t.type?k:Ie(t.type,k),L);f.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(E(163))}}catch(S){Z(t,t.return,S)}if(e=t.sibling,e!==null){e.return=t.return,_=e;break}_=t.return}return w=Xs,Xs=!1,w}function mr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Pi(t,n,o)}l=l.next}while(l!==r)}}function ro(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Fi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function vf(e){var t=e.alternate;t!==null&&(e.alternate=null,vf(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Xe],delete t[_r],delete t[wi],delete t[Rm],delete t[Om])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function yf(e){return e.tag===5||e.tag===3||e.tag===4}function Zs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||yf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Mi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=jl));else if(r!==4&&(e=e.child,e!==null))for(Mi(e,t,n),e=e.sibling;e!==null;)Mi(e,t,n),e=e.sibling}function zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(zi(e,t,n),e=e.sibling;e!==null;)zi(e,t,n),e=e.sibling}var re=null,Ae=!1;function ht(e,t,n){for(n=n.child;n!==null;)gf(e,t,n),n=n.sibling}function gf(e,t,n){if(Ze&&typeof Ze.onCommitFiberUnmount=="function")try{Ze.onCommitFiberUnmount(Xl,n)}catch{}switch(n.tag){case 5:ae||En(n,t);case 6:var r=re,l=Ae;re=null,ht(e,t,n),re=r,Ae=l,re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?Oo(e.parentNode,n):e.nodeType===1&&Oo(e,n),Er(e)):Oo(re,n.stateNode));break;case 4:r=re,l=Ae,re=n.stateNode.containerInfo,Ae=!0,ht(e,t,n),re=r,Ae=l;break;case 0:case 11:case 14:case 15:if(!ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Pi(n,t,i),l=l.next}while(l!==r)}ht(e,t,n);break;case 1:if(!ae&&(En(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Z(n,t,u)}ht(e,t,n);break;case 21:ht(e,t,n);break;case 22:n.mode&1?(ae=(r=ae)||n.memoizedState!==null,ht(e,t,n),ae=r):ht(e,t,n);break;default:ht(e,t,n)}}function Js(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Gm),t.forEach(function(r){var l=rh.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function De(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Zm(r/1960))-r,10e?16:e,Ct===null)var r=!1;else{if(e=Ct,Ct=null,Bl=0,z&6)throw Error(E(331));var l=z;for(z|=4,_=e.current;_!==null;){var o=_,i=o.child;if(_.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sJ()-Fu?Gt(e,0):Pu|=n),Se(e,t)}function Tf(e,t){t===0&&(e.mode&1?(t=Yr,Yr<<=1,!(Yr&130023424)&&(Yr=4194304)):t=1);var n=pe();e=st(e,t),e!==null&&(Dr(e,t,n),Se(e,n))}function nh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Tf(e,n)}function rh(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(E(314))}r!==null&&r.delete(t),Tf(e,n)}var _f;_f=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ge.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,Vm(e,t,n);ye=!!(e.flags&131072)}else ye=!1,Q&&t.flags&1048576&&Lc(t,Pl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;vl(e,t),e=t.pendingProps;var l=Fn(t,ce.current);Ln(t,n),l=Tu(null,t,r,e,l,n);var o=_u();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,we(r)?(o=!0,Rl(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ku(t),l.updater=to,t.stateNode=l,l._reactInternals=t,Ni(t,r,e,n),t=ji(null,t,r,!0,o,n)):(t.tag=0,Q&&o&&mu(t),de(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(vl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=oh(r),e=Ie(r,e),l){case 0:t=_i(null,t,r,e,n);break e;case 1:t=Ks(null,t,r,e,n);break e;case 11:t=Vs(null,t,r,e,n);break e;case 14:t=Qs(null,t,r,Ie(r.type,e),n);break e}throw Error(E(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),_i(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Ks(e,t,r,l,n);case 3:e:{if(cf(t),e===null)throw Error(E(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Fc(e,t),zl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=Dn(Error(E(423)),t),t=Gs(e,t,r,n,l);break e}else if(r!==l){l=Dn(Error(E(424)),t),t=Gs(e,t,r,n,l);break e}else for(Ee=jt(t.stateNode.containerInfo.firstChild),xe=t,Q=!0,Ue=null,n=Dc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Mn(),r===l){t=at(e,t,n);break e}de(e,t,r,n)}t=t.child}return t;case 5:return Ic(t),e===null&&Ei(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,yi(r,l)?i=null:o!==null&&yi(r,o)&&(t.flags|=32),af(e,t),de(e,t,i,n),t.child;case 6:return e===null&&Ei(t),null;case 13:return ff(e,t,n);case 4:return Eu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=zn(t,null,r,n):de(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Vs(e,t,r,l,n);case 7:return de(e,t,t.pendingProps,n),t.child;case 8:return de(e,t,t.pendingProps.children,n),t.child;case 12:return de(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,B(Fl,r._currentValue),r._currentValue=i,o!==null)if(We(o.value,i)){if(o.children===l.children&&!ge.current){t=at(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=lt(-1,n&-n),s.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var d=a.pending;d===null?s.next=s:(s.next=d.next,d.next=s),a.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),xi(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(E(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),xi(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}de(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Ln(t,n),l=ze(l),r=r(l),t.flags|=1,de(e,t,r,n),t.child;case 14:return r=t.type,l=Ie(r,t.pendingProps),l=Ie(r.type,l),Qs(e,t,r,l,n);case 15:return uf(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),vl(e,t),t.tag=1,we(r)?(e=!0,Rl(t)):e=!1,Ln(t,n),zc(t,r,l),Ni(t,r,l,n),ji(null,t,r,!0,e,n);case 19:return df(e,t,n);case 22:return sf(e,t,n)}throw Error(E(156,t.tag))};function jf(e,t){return ba(e,t)}function lh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Fe(e,t,n,r){return new lh(e,t,n,r)}function Du(e){return e=e.prototype,!(!e||!e.isReactComponent)}function oh(e){if(typeof e=="function")return Du(e)?1:0;if(e!=null){if(e=e.$$typeof,e===nu)return 11;if(e===ru)return 14}return 2}function Pt(e,t){var n=e.alternate;return n===null?(n=Fe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function wl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Du(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case pn:return Yt(n.children,l,o,t);case tu:i=8,l|=8;break;case Xo:return e=Fe(12,n,t,l|2),e.elementType=Xo,e.lanes=o,e;case Zo:return e=Fe(13,n,t,l),e.elementType=Zo,e.lanes=o,e;case Jo:return e=Fe(19,n,t,l),e.elementType=Jo,e.lanes=o,e;case $a:return oo(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ma:i=10;break e;case za:i=9;break e;case nu:i=11;break e;case ru:i=14;break e;case gt:i=16,r=null;break e}throw Error(E(130,e==null?e:typeof e,""))}return t=Fe(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Yt(e,t,n,r){return e=Fe(7,e,r,t),e.lanes=n,e}function oo(e,t,n,r){return e=Fe(22,e,r,t),e.elementType=$a,e.lanes=n,e.stateNode={isHidden:!1},e}function Ao(e,t,n){return e=Fe(6,e,null,t),e.lanes=n,e}function Uo(e,t,n){return t=Fe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function ih(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=So(0),this.expirationTimes=So(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=So(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Iu(e,t,n,r,l,o,i,u,s){return e=new ih(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Fe(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ku(o),e}function uh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Pf)}catch(e){console.error(e)}}Pf(),La.exports=Ne;var Ff=La.exports;const Cn=Gl(Ff);var oa=Ff;Go.createRoot=oa.createRoot,Go.hydrateRoot=oa.hydrateRoot;var Mf={exports:{}};/*! +`+o.stack}return{value:e,source:t,stack:l,digest:null}}function Do(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function Ti(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var Bm=typeof WeakMap=="function"?WeakMap:Map;function lf(e,t,n){n=lt(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){Ul||(Ul=!0,$i=r),Ti(e,t)},n}function of(e,t,n){n=lt(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){Ti(e,t)}}var o=e.stateNode;return o!==null&&typeof o.componentDidCatch=="function"&&(n.callback=function(){Ti(e,t),typeof r!="function"&&(Lt===null?Lt=new Set([this]):Lt.add(this));var i=t.stack;this.componentDidCatch(t.value,{componentStack:i!==null?i:""})}),n}function Bs(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Bm;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=th.bind(null,e,t,n),t.then(e,e))}function Hs(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Ws(e,t,n,r,l){return e.mode&1?(e.flags|=65536,e.lanes=l,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=lt(-1,1),t.tag=2,Rt(n,t,1))),n.lanes|=1),e)}var Hm=dt.ReactCurrentOwner,ye=!1;function de(e,t,n,r){t.child=e===null?Dc(t,null,n,r):zn(t,e.child,n,r)}function Vs(e,t,n,r,l){n=n.render;var o=t.ref;return Rn(t,l),r=Tu(e,t,n,r,o,l),n=_u(),e!==null&&!ye?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,at(e,t,l)):(Q&&n&&mu(t),t.flags|=1,de(e,t,r,l),t.child)}function Qs(e,t,n,r,l){if(e===null){var o=n.type;return typeof o=="function"&&!Du(o)&&o.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=o,uf(e,t,o,r,l)):(e=wl(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(o=e.child,!(e.lanes&l)){var i=o.memoizedProps;if(n=n.compare,n=n!==null?n:Cr,n(i,r)&&e.ref===t.ref)return at(e,t,l)}return t.flags|=1,e=Pt(o,r),e.ref=t.ref,e.return=t,t.child=e}function uf(e,t,n,r,l){if(e!==null){var o=e.memoizedProps;if(Cr(o,r)&&e.ref===t.ref)if(ye=!1,t.pendingProps=r=o,(e.lanes&l)!==0)e.flags&131072&&(ye=!0);else return t.lanes=e.lanes,at(e,t,l)}return _i(e,t,n,r,l)}function sf(e,t,n){var r=t.pendingProps,l=r.children,o=e!==null?e.memoizedState:null;if(r.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},B(xn,ke),ke|=n;else{if(!(n&1073741824))return e=o!==null?o.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,B(xn,ke),ke|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=o!==null?o.baseLanes:n,B(xn,ke),ke|=r}else o!==null?(r=o.baseLanes|n,t.memoizedState=null):r=n,B(xn,ke),ke|=r;return de(e,t,l,n),t.child}function af(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function _i(e,t,n,r,l){var o=we(n)?Zt:ce.current;return o=Fn(t,o),Rn(t,l),n=Tu(e,t,n,r,o,l),r=_u(),e!==null&&!ye?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,at(e,t,l)):(Q&&r&&mu(t),t.flags|=1,de(e,t,n,l),t.child)}function Ks(e,t,n,r,l){if(we(n)){var o=!0;Ll(t)}else o=!1;if(Rn(t,l),t.stateNode===null)vl(e,t),zc(t,n,r),Ni(t,n,r,l),r=!0;else if(e===null){var i=t.stateNode,u=t.memoizedProps;i.props=u;var s=i.context,a=n.contextType;typeof a=="object"&&a!==null?a=ze(a):(a=we(n)?Zt:ce.current,a=Fn(t,a));var d=n.getDerivedStateFromProps,m=typeof d=="function"||typeof i.getSnapshotBeforeUpdate=="function";m||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(u!==r||s!==a)&&Ds(t,i,r,a),wt=!1;var p=t.memoizedState;i.state=p,zl(t,r,i,l),s=t.memoizedState,u!==r||p!==s||ge.current||wt?(typeof d=="function"&&(Ci(t,n,d,r),s=t.memoizedState),(u=wt||$s(t,n,u,r,p,s,a))?(m||typeof i.UNSAFE_componentWillMount!="function"&&typeof i.componentWillMount!="function"||(typeof i.componentWillMount=="function"&&i.componentWillMount(),typeof i.UNSAFE_componentWillMount=="function"&&i.UNSAFE_componentWillMount()),typeof i.componentDidMount=="function"&&(t.flags|=4194308)):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=s),i.props=r,i.state=s,i.context=a,r=u):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{i=t.stateNode,Fc(e,t),u=t.memoizedProps,a=t.type===t.elementType?u:Ie(t.type,u),i.props=a,m=t.pendingProps,p=i.context,s=n.contextType,typeof s=="object"&&s!==null?s=ze(s):(s=we(n)?Zt:ce.current,s=Fn(t,s));var g=n.getDerivedStateFromProps;(d=typeof g=="function"||typeof i.getSnapshotBeforeUpdate=="function")||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(u!==m||p!==s)&&Ds(t,i,r,s),wt=!1,p=t.memoizedState,i.state=p,zl(t,r,i,l);var w=t.memoizedState;u!==m||p!==w||ge.current||wt?(typeof g=="function"&&(Ci(t,n,g,r),w=t.memoizedState),(a=wt||$s(t,n,a,r,p,w,s)||!1)?(d||typeof i.UNSAFE_componentWillUpdate!="function"&&typeof i.componentWillUpdate!="function"||(typeof i.componentWillUpdate=="function"&&i.componentWillUpdate(r,w,s),typeof i.UNSAFE_componentWillUpdate=="function"&&i.UNSAFE_componentWillUpdate(r,w,s)),typeof i.componentDidUpdate=="function"&&(t.flags|=4),typeof i.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof i.componentDidUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=w),i.props=r,i.state=w,i.context=s,r=a):(typeof i.componentDidUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),r=!1)}return ji(e,t,n,r,o,l)}function ji(e,t,n,r,l,o){af(e,t);var i=(t.flags&128)!==0;if(!r&&!i)return l&&Os(t,n,!1),at(e,t,o);r=t.stateNode,Hm.current=t;var u=i&&typeof n.getDerivedStateFromError!="function"?null:r.render();return t.flags|=1,e!==null&&i?(t.child=zn(t,e.child,null,o),t.child=zn(t,null,u,o)):de(e,t,u,o),t.memoizedState=r.state,l&&Os(t,n,!0),t.child}function cf(e){var t=e.stateNode;t.pendingContext?Ls(e,t.pendingContext,t.pendingContext!==t.context):t.context&&Ls(e,t.context,!1),Eu(e,t.containerInfo)}function Gs(e,t,n,r,l){return Mn(),vu(l),t.flags|=256,de(e,t,n,r),t.child}var Ri={dehydrated:null,treeContext:null,retryLane:0};function Li(e){return{baseLanes:e,cachePool:null,transitions:null}}function ff(e,t,n){var r=t.pendingProps,l=G.current,o=!1,i=(t.flags&128)!==0,u;if((u=i)||(u=e!==null&&e.memoizedState===null?!1:(l&2)!==0),u?(o=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(l|=1),B(G,l&1),e===null)return Ei(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?(t.mode&1?e.data==="$!"?t.lanes=8:t.lanes=1073741824:t.lanes=1,null):(i=r.children,e=r.fallback,o?(r=t.mode,o=t.child,i={mode:"hidden",children:i},!(r&1)&&o!==null?(o.childLanes=0,o.pendingProps=i):o=oo(i,r,0,null),e=Yt(e,r,n,null),o.return=t,e.return=t,o.sibling=e,t.child=o,t.child.memoizedState=Li(n),t.memoizedState=Ri,e):Lu(t,i));if(l=e.memoizedState,l!==null&&(u=l.dehydrated,u!==null))return Wm(e,t,i,r,u,l,n);if(o){o=r.fallback,i=t.mode,l=e.child,u=l.sibling;var s={mode:"hidden",children:r.children};return!(i&1)&&t.child!==l?(r=t.child,r.childLanes=0,r.pendingProps=s,t.deletions=null):(r=Pt(l,s),r.subtreeFlags=l.subtreeFlags&14680064),u!==null?o=Pt(u,o):(o=Yt(o,i,n,null),o.flags|=2),o.return=t,r.return=t,r.sibling=o,t.child=r,r=o,o=t.child,i=e.child.memoizedState,i=i===null?Li(n):{baseLanes:i.baseLanes|n,cachePool:null,transitions:i.transitions},o.memoizedState=i,o.childLanes=e.childLanes&~n,t.memoizedState=Ri,r}return o=e.child,e=o.sibling,r=Pt(o,{mode:"visible",children:r.children}),!(t.mode&1)&&(r.lanes=n),r.return=t,r.sibling=null,e!==null&&(n=t.deletions,n===null?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=r,t.memoizedState=null,r}function Lu(e,t){return t=oo({mode:"visible",children:t},e.mode,0,null),t.return=e,e.child=t}function nl(e,t,n,r){return r!==null&&vu(r),zn(t,e.child,null,n),e=Lu(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function Wm(e,t,n,r,l,o,i){if(n)return t.flags&256?(t.flags&=-257,r=Do(Error(E(422))),nl(e,t,i,r)):t.memoizedState!==null?(t.child=e.child,t.flags|=128,null):(o=r.fallback,l=t.mode,r=oo({mode:"visible",children:r.children},l,0,null),o=Yt(o,l,i,null),o.flags|=2,r.return=t,o.return=t,r.sibling=o,t.child=r,t.mode&1&&zn(t,e.child,null,i),t.child.memoizedState=Li(i),t.memoizedState=Ri,o);if(!(t.mode&1))return nl(e,t,i,null);if(l.data==="$!"){if(r=l.nextSibling&&l.nextSibling.dataset,r)var u=r.dgst;return r=u,o=Error(E(419)),r=Do(o,r,void 0),nl(e,t,i,r)}if(u=(i&e.childLanes)!==0,ye||u){if(r=ne,r!==null){switch(i&-i){case 4:l=2;break;case 16:l=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:l=32;break;case 536870912:l=268435456;break;default:l=0}l=l&(r.suspendedLanes|i)?0:l,l!==0&&l!==o.retryLane&&(o.retryLane=l,st(e,l),He(r,e,l,-1))}return $u(),r=Do(Error(E(421))),nl(e,t,i,r)}return l.data==="$?"?(t.flags|=128,t.child=e.child,t=nh.bind(null,e),l._reactRetry=t,null):(e=o.treeContext,Ee=jt(l.nextSibling),xe=t,Q=!0,Ue=null,e!==null&&(Le[Oe++]=nt,Le[Oe++]=rt,Le[Oe++]=Jt,nt=e.id,rt=e.overflow,Jt=t),t=Lu(t,r.children),t.flags|=4096,t)}function Ys(e,t,n){e.lanes|=t;var r=e.alternate;r!==null&&(r.lanes|=t),xi(e.return,t,n)}function Io(e,t,n,r,l){var o=e.memoizedState;o===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:l}:(o.isBackwards=t,o.rendering=null,o.renderingStartTime=0,o.last=r,o.tail=n,o.tailMode=l)}function df(e,t,n){var r=t.pendingProps,l=r.revealOrder,o=r.tail;if(de(e,t,r.children,n),r=G.current,r&2)r=r&1|2,t.flags|=128;else{if(e!==null&&e.flags&128)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Ys(e,n,t);else if(e.tag===19)Ys(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if(B(G,r),!(t.mode&1))t.memoizedState=null;else switch(l){case"forwards":for(n=t.child,l=null;n!==null;)e=n.alternate,e!==null&&$l(e)===null&&(l=n),n=n.sibling;n=l,n===null?(l=t.child,t.child=null):(l=n.sibling,n.sibling=null),Io(t,!1,l,n,o);break;case"backwards":for(n=null,l=t.child,t.child=null;l!==null;){if(e=l.alternate,e!==null&&$l(e)===null){t.child=l;break}e=l.sibling,l.sibling=n,n=l,l=e}Io(t,!0,n,null,o);break;case"together":Io(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function vl(e,t){!(t.mode&1)&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}function at(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),bt|=t.lanes,!(n&t.childLanes))return null;if(e!==null&&t.child!==e.child)throw Error(E(153));if(t.child!==null){for(e=t.child,n=Pt(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=Pt(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function Vm(e,t,n){switch(t.tag){case 3:cf(t),Mn();break;case 5:Ic(t);break;case 1:we(t.type)&&Ll(t);break;case 4:Eu(t,t.stateNode.containerInfo);break;case 10:var r=t.type._context,l=t.memoizedProps.value;B(Fl,r._currentValue),r._currentValue=l;break;case 13:if(r=t.memoizedState,r!==null)return r.dehydrated!==null?(B(G,G.current&1),t.flags|=128,null):n&t.child.childLanes?ff(e,t,n):(B(G,G.current&1),e=at(e,t,n),e!==null?e.sibling:null);B(G,G.current&1);break;case 19:if(r=(n&t.childLanes)!==0,e.flags&128){if(r)return df(e,t,n);t.flags|=128}if(l=t.memoizedState,l!==null&&(l.rendering=null,l.tail=null,l.lastEffect=null),B(G,G.current),r)break;return null;case 22:case 23:return t.lanes=0,sf(e,t,n)}return at(e,t,n)}var pf,Oi,mf,hf;pf=function(e,t){for(var n=t.child;n!==null;){if(n.tag===5||n.tag===6)e.appendChild(n.stateNode);else if(n.tag!==4&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}};Oi=function(){};mf=function(e,t,n,r){var l=e.memoizedProps;if(l!==r){e=t.stateNode,Kt(Je.current);var o=null;switch(n){case"input":l=bo(e,l),r=bo(e,r),o=[];break;case"select":l=X({},l,{value:void 0}),r=X({},r,{value:void 0}),o=[];break;case"textarea":l=ni(e,l),r=ni(e,r),o=[];break;default:typeof l.onClick!="function"&&typeof r.onClick=="function"&&(e.onclick=jl)}li(n,r);var i;n=null;for(a in l)if(!r.hasOwnProperty(a)&&l.hasOwnProperty(a)&&l[a]!=null)if(a==="style"){var u=l[a];for(i in u)u.hasOwnProperty(i)&&(n||(n={}),n[i]="")}else a!=="dangerouslySetInnerHTML"&&a!=="children"&&a!=="suppressContentEditableWarning"&&a!=="suppressHydrationWarning"&&a!=="autoFocus"&&(yr.hasOwnProperty(a)?o||(o=[]):(o=o||[]).push(a,null));for(a in r){var s=r[a];if(u=l!=null?l[a]:void 0,r.hasOwnProperty(a)&&s!==u&&(s!=null||u!=null))if(a==="style")if(u){for(i in u)!u.hasOwnProperty(i)||s&&s.hasOwnProperty(i)||(n||(n={}),n[i]="");for(i in s)s.hasOwnProperty(i)&&u[i]!==s[i]&&(n||(n={}),n[i]=s[i])}else n||(o||(o=[]),o.push(a,n)),n=s;else a==="dangerouslySetInnerHTML"?(s=s?s.__html:void 0,u=u?u.__html:void 0,s!=null&&u!==s&&(o=o||[]).push(a,s)):a==="children"?typeof s!="string"&&typeof s!="number"||(o=o||[]).push(a,""+s):a!=="suppressContentEditableWarning"&&a!=="suppressHydrationWarning"&&(yr.hasOwnProperty(a)?(s!=null&&a==="onScroll"&&W("scroll",e),o||u===s||(o=[])):(o=o||[]).push(a,s))}n&&(o=o||[]).push("style",n);var a=o;(t.updateQueue=a)&&(t.flags|=4)}};hf=function(e,t,n,r){n!==r&&(t.flags|=4)};function tr(e,t){if(!Q)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;n!==null;)n.alternate!==null&&(r=n),n=n.sibling;r===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:r.sibling=null}}function se(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,r=0;if(t)for(var l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags&14680064,r|=l.flags&14680064,l.return=e,l=l.sibling;else for(l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags,r|=l.flags,l.return=e,l=l.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function Qm(e,t,n){var r=t.pendingProps;switch(hu(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return se(t),null;case 1:return we(t.type)&&Rl(),se(t),null;case 3:return r=t.stateNode,$n(),V(ge),V(ce),Cu(),r.pendingContext&&(r.context=r.pendingContext,r.pendingContext=null),(e===null||e.child===null)&&(el(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&!(t.flags&256)||(t.flags|=1024,Ue!==null&&(Ai(Ue),Ue=null))),Oi(e,t),se(t),null;case 5:xu(t);var l=Kt(Rr.current);if(n=t.type,e!==null&&t.stateNode!=null)mf(e,t,n,r,l),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!r){if(t.stateNode===null)throw Error(E(166));return se(t),null}if(e=Kt(Je.current),el(t)){r=t.stateNode,n=t.type;var o=t.memoizedProps;switch(r[Xe]=t,r[_r]=o,e=(t.mode&1)!==0,n){case"dialog":W("cancel",r),W("close",r);break;case"iframe":case"object":case"embed":W("load",r);break;case"video":case"audio":for(l=0;l<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Xe]=t,e[_r]=r,pf(e,t,!1,!1),t.stateNode=e;e:{switch(i=oi(n,r),n){case"dialog":W("cancel",e),W("close",e),l=r;break;case"iframe":case"object":case"embed":W("load",e),l=r;break;case"video":case"audio":for(l=0;lIn&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304)}else{if(!r)if(e=$l(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),tr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!Q)return se(t),null}else 2*J()-o.renderingStartTime>In&&n!==1073741824&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=J(),t.sibling=null,n=G.current,B(G,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return zu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ke&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(E(156,t.tag))}function Km(e,t){switch(hu(t),t.tag){case 1:return we(t.type)&&Rl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return $n(),V(ge),V(ce),Cu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return xu(t),null;case 13:if(V(G),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(E(340));Mn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(G),null;case 4:return $n(),null;case 10:return wu(t.type._context),null;case 22:case 23:return zu(),null;case 24:return null;default:return null}}var rl=!1,ae=!1,Gm=typeof WeakSet=="function"?WeakSet:Set,_=null;function En(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Z(e,t,r)}else n.current=null}function Pi(e,t,n){try{n()}catch(r){Z(e,t,r)}}var Xs=!1;function Ym(e,t){if(hi=Nl,e=gc(),pu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,a=0,d=0,m=e,p=null;t:for(;;){for(var g;m!==n||l!==0&&m.nodeType!==3||(u=i+l),m!==o||r!==0&&m.nodeType!==3||(s=i+r),m.nodeType===3&&(i+=m.nodeValue.length),(g=m.firstChild)!==null;)p=m,m=g;for(;;){if(m===e)break t;if(p===n&&++a===l&&(u=i),p===o&&++d===r&&(s=i),(g=m.nextSibling)!==null)break;m=p,p=m.parentNode}m=g}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(vi={focusedElem:e,selectionRange:n},Nl=!1,_=t;_!==null;)if(t=_,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_=e;else for(;_!==null;){t=_;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var k=w.memoizedProps,R=w.memoizedState,f=t.stateNode,c=f.getSnapshotBeforeUpdate(t.elementType===t.type?k:Ie(t.type,k),R);f.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(E(163))}}catch(S){Z(t,t.return,S)}if(e=t.sibling,e!==null){e.return=t.return,_=e;break}_=t.return}return w=Xs,Xs=!1,w}function mr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Pi(t,n,o)}l=l.next}while(l!==r)}}function ro(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Fi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function vf(e){var t=e.alternate;t!==null&&(e.alternate=null,vf(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Xe],delete t[_r],delete t[wi],delete t[Lm],delete t[Om])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function yf(e){return e.tag===5||e.tag===3||e.tag===4}function Zs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||yf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Mi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=jl));else if(r!==4&&(e=e.child,e!==null))for(Mi(e,t,n),e=e.sibling;e!==null;)Mi(e,t,n),e=e.sibling}function zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(zi(e,t,n),e=e.sibling;e!==null;)zi(e,t,n),e=e.sibling}var re=null,Ae=!1;function ht(e,t,n){for(n=n.child;n!==null;)gf(e,t,n),n=n.sibling}function gf(e,t,n){if(Ze&&typeof Ze.onCommitFiberUnmount=="function")try{Ze.onCommitFiberUnmount(Xl,n)}catch{}switch(n.tag){case 5:ae||En(n,t);case 6:var r=re,l=Ae;re=null,ht(e,t,n),re=r,Ae=l,re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?Oo(e.parentNode,n):e.nodeType===1&&Oo(e,n),Er(e)):Oo(re,n.stateNode));break;case 4:r=re,l=Ae,re=n.stateNode.containerInfo,Ae=!0,ht(e,t,n),re=r,Ae=l;break;case 0:case 11:case 14:case 15:if(!ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Pi(n,t,i),l=l.next}while(l!==r)}ht(e,t,n);break;case 1:if(!ae&&(En(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Z(n,t,u)}ht(e,t,n);break;case 21:ht(e,t,n);break;case 22:n.mode&1?(ae=(r=ae)||n.memoizedState!==null,ht(e,t,n),ae=r):ht(e,t,n);break;default:ht(e,t,n)}}function Js(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Gm),t.forEach(function(r){var l=rh.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function De(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Zm(r/1960))-r,10e?16:e,Ct===null)var r=!1;else{if(e=Ct,Ct=null,Bl=0,z&6)throw Error(E(331));var l=z;for(z|=4,_=e.current;_!==null;){var o=_,i=o.child;if(_.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sJ()-Fu?Gt(e,0):Pu|=n),Se(e,t)}function Tf(e,t){t===0&&(e.mode&1?(t=Yr,Yr<<=1,!(Yr&130023424)&&(Yr=4194304)):t=1);var n=pe();e=st(e,t),e!==null&&(Dr(e,t,n),Se(e,n))}function nh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Tf(e,n)}function rh(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(E(314))}r!==null&&r.delete(t),Tf(e,n)}var _f;_f=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ge.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,Vm(e,t,n);ye=!!(e.flags&131072)}else ye=!1,Q&&t.flags&1048576&&Rc(t,Pl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;vl(e,t),e=t.pendingProps;var l=Fn(t,ce.current);Rn(t,n),l=Tu(null,t,r,e,l,n);var o=_u();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,we(r)?(o=!0,Ll(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ku(t),l.updater=to,t.stateNode=l,l._reactInternals=t,Ni(t,r,e,n),t=ji(null,t,r,!0,o,n)):(t.tag=0,Q&&o&&mu(t),de(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(vl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=oh(r),e=Ie(r,e),l){case 0:t=_i(null,t,r,e,n);break e;case 1:t=Ks(null,t,r,e,n);break e;case 11:t=Vs(null,t,r,e,n);break e;case 14:t=Qs(null,t,r,Ie(r.type,e),n);break e}throw Error(E(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),_i(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Ks(e,t,r,l,n);case 3:e:{if(cf(t),e===null)throw Error(E(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Fc(e,t),zl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=Dn(Error(E(423)),t),t=Gs(e,t,r,n,l);break e}else if(r!==l){l=Dn(Error(E(424)),t),t=Gs(e,t,r,n,l);break e}else for(Ee=jt(t.stateNode.containerInfo.firstChild),xe=t,Q=!0,Ue=null,n=Dc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Mn(),r===l){t=at(e,t,n);break e}de(e,t,r,n)}t=t.child}return t;case 5:return Ic(t),e===null&&Ei(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,yi(r,l)?i=null:o!==null&&yi(r,o)&&(t.flags|=32),af(e,t),de(e,t,i,n),t.child;case 6:return e===null&&Ei(t),null;case 13:return ff(e,t,n);case 4:return Eu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=zn(t,null,r,n):de(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Vs(e,t,r,l,n);case 7:return de(e,t,t.pendingProps,n),t.child;case 8:return de(e,t,t.pendingProps.children,n),t.child;case 12:return de(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,B(Fl,r._currentValue),r._currentValue=i,o!==null)if(We(o.value,i)){if(o.children===l.children&&!ge.current){t=at(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=lt(-1,n&-n),s.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var d=a.pending;d===null?s.next=s:(s.next=d.next,d.next=s),a.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),xi(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(E(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),xi(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}de(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Rn(t,n),l=ze(l),r=r(l),t.flags|=1,de(e,t,r,n),t.child;case 14:return r=t.type,l=Ie(r,t.pendingProps),l=Ie(r.type,l),Qs(e,t,r,l,n);case 15:return uf(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),vl(e,t),t.tag=1,we(r)?(e=!0,Ll(t)):e=!1,Rn(t,n),zc(t,r,l),Ni(t,r,l,n),ji(null,t,r,!0,e,n);case 19:return df(e,t,n);case 22:return sf(e,t,n)}throw Error(E(156,t.tag))};function jf(e,t){return ba(e,t)}function lh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Fe(e,t,n,r){return new lh(e,t,n,r)}function Du(e){return e=e.prototype,!(!e||!e.isReactComponent)}function oh(e){if(typeof e=="function")return Du(e)?1:0;if(e!=null){if(e=e.$$typeof,e===nu)return 11;if(e===ru)return 14}return 2}function Pt(e,t){var n=e.alternate;return n===null?(n=Fe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function wl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Du(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case pn:return Yt(n.children,l,o,t);case tu:i=8,l|=8;break;case Xo:return e=Fe(12,n,t,l|2),e.elementType=Xo,e.lanes=o,e;case Zo:return e=Fe(13,n,t,l),e.elementType=Zo,e.lanes=o,e;case Jo:return e=Fe(19,n,t,l),e.elementType=Jo,e.lanes=o,e;case $a:return oo(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ma:i=10;break e;case za:i=9;break e;case nu:i=11;break e;case ru:i=14;break e;case gt:i=16,r=null;break e}throw Error(E(130,e==null?e:typeof e,""))}return t=Fe(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Yt(e,t,n,r){return e=Fe(7,e,r,t),e.lanes=n,e}function oo(e,t,n,r){return e=Fe(22,e,r,t),e.elementType=$a,e.lanes=n,e.stateNode={isHidden:!1},e}function Ao(e,t,n){return e=Fe(6,e,null,t),e.lanes=n,e}function Uo(e,t,n){return t=Fe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function ih(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=So(0),this.expirationTimes=So(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=So(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Iu(e,t,n,r,l,o,i,u,s){return e=new ih(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Fe(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ku(o),e}function uh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Pf)}catch(e){console.error(e)}}Pf(),Ra.exports=Ne;var Ff=Ra.exports;const Cn=Gl(Ff);var oa=Ff;Go.createRoot=oa.createRoot,Go.hydrateRoot=oa.hydrateRoot;var Mf={exports:{}};/*! Copyright (c) 2018 Jed Watson. Licensed under the MIT License (MIT), see http://jedwatson.github.io/classnames -*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function ph(e){var t=mh(e,"string");return typeof t=="symbol"?t:String(t)}function mh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function hh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var d=arguments.length,m=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Ah(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Uh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function po(e,t){return y.useMemo(()=>Uh(e,t),[e,t])}function Bh(e){return e&&"setState"in e?Cn.findDOMNode(e):e??null}const Hh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const m=y.useRef(null),p=po(m,s),g=N=>{p(Bh(N))},w=N=>T=>{N&&m.current&&N(m.current,T)},k=y.useCallback(w(e),[e]),L=y.useCallback(w(t),[t]),f=y.useCallback(w(n),[n]),c=y.useCallback(w(r),[r]),h=y.useCallback(w(l),[l]),S=y.useCallback(w(o),[o]),C=y.useCallback(w(i),[i]);return v.jsx(Mh,{ref:d,...a,onEnter:k,onEntered:f,onEntering:L,onExit:c,onExited:S,onExiting:h,addEndListener:C,nodeRef:m,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Wt.cloneElement(u,{ref:g})})}),Wh=Hh;function Vh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Vh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Vf=e=>y.forwardRef((t,n)=>v.jsx("div",{...t,ref:n,className:M(t.className,e)})),Qf=Vf("h4");Qf.displayName="DivStyledAsH4";const Kf=y.forwardRef(({className:e,bsPrefix:t,as:n=Qf,...r},l)=>(t=H(t,"alert-heading"),v.jsx(n,{ref:l,className:M(e,t),...r})));Kf.displayName="AlertHeading";const Qh=Kf;function Kh(){return y.useState(null)}function Gh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Yh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Xh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Zh=typeof document<"u",ca=Zh||Xh?y.useLayoutEffect:y.useEffect,Jh=["as","disabled"];function qh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function bh(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&bh(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},m=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:m},a]}const ev=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=qh(e,Jh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return v.jsx(i,Object.assign({},l,o,{ref:t}))});ev.displayName="Button";const tv=["onKeyDown"];function nv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function rv(e){return!e||e.trim()==="#"}const Gf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=nv(e,tv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return rv(r.href)||r.role==="button"?v.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):v.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Gf.displayName="Anchor";const lv=Gf,Yf=y.forwardRef(({className:e,bsPrefix:t,as:n=lv,...r},l)=>(t=H(t,"alert-link"),v.jsx(n,{ref:l,className:M(e,t),...r})));Yf.displayName="AlertLink";const ov=Yf,iv={[St]:"show",[Ht]:"show"},Xf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Ah(s),r==null||r(s,a)},[r]);return v.jsx(Wh,{ref:o,addEndListener:Ih,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,iv[s],n[s])})})});Xf.displayName="Fade";const Ql=Xf,uv={"aria-label":ot.string,onClick:ot.func,variant:ot.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>v.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=uv;const Zf=Wu,Jf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:m=Ql,...p}=vh(e,{show:"onClose"}),g=H(n,"alert"),w=Pe(f=>{a&&a(!1,f)}),k=m===!0?Ql:m,L=v.jsxs("div",{role:"alert",...k?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&v.jsx(Zf,{onClick:w,"aria-label":l,variant:o}),u]});return k?v.jsx(k,{unmountOnExit:!0,...p,ref:void 0,in:r,children:L}):r?L:null});Jf.displayName="Alert";const fa=Object.assign(Jf,{Link:ov,Heading:Qh}),qf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:m}]=Hu({tagName:e,disabled:o,...u}),p=m;return v.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});qf.displayName="Button";const Mr=qf;function sv(e){const t=y.useRef(e);return t.current=e,t}function bf(e){const t=sv(e);y.useEffect(()=>()=>t.current(),[])}function av(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function cv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function fv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=$f(),o=Df(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,m,p;typeof a=="object"&&a!=null?{span:d,offset:m,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),m!=null&&u.push(`offset${g}-${m}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const ed=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=fv(e);return v.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});ed.displayName="Col";const Vu=ed,td=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return v.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});td.displayName="Container";const dv=td;var pv=Function.prototype.bind.call(Function.prototype.call,[].slice);function cn(e,t){return pv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const mv="data-rr-ui-";function hv(e){return`${mv}${e}`}const nd=y.createContext(Wn?window:void 0);nd.Provider;function Qu(){return y.useContext(nd)}const vv={type:ot.string,tooltip:ot.bool,as:ot.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>v.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=vv;const rd=Ku,yv=y.createContext({}),ct=yv,ld=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ct);return t=H(t,"form-check-input"),v.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});ld.displayName="FormCheckInput";const od=ld,id=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-check-label"),v.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});id.displayName="FormCheckLabel";const Ki=id,ud=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:m,style:p,title:g="",type:w="checkbox",label:k,children:L,as:f="input",...c},h)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ct),C=y.useMemo(()=>({controlId:e||S}),[S,e]),N=!L&&k!=null&&k!==!1||cv(L,Ki),T=v.jsx(od,{...c,type:w==="switch"?"checkbox":w,ref:h,isValid:i,isInvalid:u,disabled:o,as:f});return v.jsx(ct.Provider,{value:C,children:v.jsx("div",{style:p,className:M(m,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,w==="switch"&&n),children:L||v.jsxs(v.Fragment,{children:[T,N&&v.jsx(Ki,{title:g,children:k}),a&&v.jsx(rd,{type:d,tooltip:s,children:a})]})})})});ud.displayName="FormCheck";const Kl=Object.assign(ud,{Input:od,Label:Ki}),sd=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...m},p)=>{const{controlId:g}=y.useContext(ct);return e=H(e,"form-control"),v.jsx(d,{...m,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});sd.displayName="FormControl";const gv=Object.assign(sd,{Feedback:rd}),ad=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),v.jsx(n,{ref:l,className:M(e,t),...r})));ad.displayName="FormFloating";const wv=ad,cd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return v.jsx(ct.Provider,{value:l,children:v.jsx(t,{...n,ref:r})})});cd.displayName="FormGroup";const fd=cd,dd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ct);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?v.jsx(Vu,{ref:u,as:"label",className:d,htmlFor:o,...i}):v.jsx(e,{ref:u,className:d,htmlFor:o,...i})});dd.displayName="FormLabel";const Sv=dd,pd=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-range"),v.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});pd.displayName="FormRange";const kv=pd,md=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ct);return e=H(e,"form-select"),v.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});md.displayName="FormSelect";const Ev=md,hd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),v.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));hd.displayName="FormText";const xv=hd,vd=y.forwardRef((e,t)=>v.jsx(Kl,{...e,ref:t,type:"switch"}));vd.displayName="Switch";const Cv=Object.assign(vd,{Input:Kl.Input,Label:Kl.Label}),yd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),v.jsxs(fd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,v.jsx("label",{htmlFor:r,children:l})]})));yd.displayName="FloatingLabel";const Nv=yd,Tv={_ref:ot.any,validated:ot.bool,as:ot.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>v.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=Tv;const On=Object.assign(Gu,{Group:fd,Control:gv,Floating:wv,Check:Kl,Switch:Cv,Label:Sv,Text:xv,Range:kv,Select:Ev,FloatingLabel:Nv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Wn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Ho(e){e===void 0&&(e=fo());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function _v(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=hv("modal-open");class jv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return _v(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=jv,Wo=(e,t)=>Wn?e==null?(t||fo()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Lv(e,t){const n=Qu(),[r,l]=y.useState(()=>Wo(e,n==null?void 0:n.document));if(!r){const o=Wo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Wo(e);o!==r&&l(o)},[e,r]),r}function Rv({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Pe(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=po(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Ov({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Pe(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Pv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Ov({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,m=>{throw a.in||i(!0),m})}}),s=po(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?v.jsx(e,Object.assign({},n)):t?v.jsx(Pv,Object.assign({},n,{transition:t})):v.jsx(Rv,Object.assign({},n))}function Fv(e){return e.code==="Escape"||e.keyCode===27}const Mv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function zv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Vo;function $v(e){return Vo||(Vo=new Yu({ownerDocument:e==null?void 0:e.document})),Vo}function Dv(e){const t=Qu(),n=e||$v(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const gd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:m,runTransition:p,backdropTransition:g,runBackdropTransition:w,autoFocus:k=!0,enforceFocus:L=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:h,renderBackdrop:S=K=>v.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:j=()=>{},onExit:U,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:rn}=e,Qn=zv(e,Mv);const _e=Qu(),Ke=Lv(N),x=Dv(C),R=Gh(),O=Yh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>x,[x]),Wn&&!O&&n&&(fe.current=Ho(_e==null?void 0:_e.document)),n&&D&&A(!1);const je=Pe(()=>{if(x.add(),on.current=Vl(document,"keydown",mo),ln.current=Vl(document,"focus",()=>setTimeout(Le),!0),T&&T(),k){var K,Hr;const Yn=Ho((K=(Hr=x.dialog)==null?void 0:Hr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Yn&&!da(x.dialog,Yn)&&(fe.current=Yn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),on.current==null||on.current(),ln.current==null||ln.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ke||je()},[n,Ke,je]),y.useEffect(()=>{D&&qe()},[D,qe]),bf(()=>{qe()});const Le=Pe(()=>{if(!L||!R()||!x.isTopModal())return;const K=Ho(_e==null?void 0:_e.document);x.dialog&&K&&!da(x.dialog,K)&&x.dialog.focus()}),mt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),mo=Pe(K=>{s&&Fv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||j())}),ln=y.useRef(),on=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const Br=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=h?h(Br):v.jsx("div",Object.assign({},Br,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(m,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:U,onExiting:ie,onExited:Kn,onEnter:Ve,onEntering:Qe,onEntered:rn,children:Gn});let It=null;return u&&(It=S({ref:x.setBackdropRef,onClick:mt}),It=ha(g,w,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),v.jsx(v.Fragment,{children:Cn.createPortal(v.jsxs(v.Fragment,{children:[It,Gn]}),Ke)})});gd.displayName="Modal";const Iv=Object.assign(gd,{Manager:Yu});function Av(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Uv(e,t){e.classList?e.classList.add(t):Av(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Bv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const fn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Hv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Uv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Bv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Qo;function Wv(e){return Qo||(Qo=new Hv(e)),Qo}const wd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),v.jsx(n,{ref:l,className:M(e,t),...r})));wd.displayName="ModalBody";const Vv=wd,Qv=y.createContext({onHide(){}}),Sd=Qv,kd=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,m=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return v.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&m),children:v.jsx("div",{className:M(`${e}-content`,n),children:i})})});kd.displayName="ModalDialog";const Ed=kd,xd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),v.jsx(n,{ref:l,className:M(e,t),...r})));xd.displayName="ModalFooter";const Kv=xd,Gv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(Sd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return v.jsxs("div",{ref:i,...o,children:[l,n&&v.jsx(Zf,{"aria-label":e,variant:t,onClick:s})]})}),Yv=Gv,Cd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),v.jsx(Yv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Cd.displayName="ModalHeader";const Xv=Cd,Zv=Vf("h4"),Nd=y.forwardRef(({className:e,bsPrefix:t,as:n=Zv,...r},l)=>(t=H(t,"modal-title"),v.jsx(n,{ref:l,className:M(e,t),...r})));Nd.displayName="ModalTitle";const Jv=Nd;function qv(e){return v.jsx(Ql,{...e,timeout:null})}function bv(e){return v.jsx(Ql,{...e,timeout:null})}const Td=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=Ed,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:m=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:w,onShow:k,onHide:L,container:f,autoFocus:c=!0,enforceFocus:h=!0,restoreFocus:S=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:j,onEnter:U,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...rn},Qn)=>{const[_e,Ke]=y.useState({}),[x,R]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,je]=Kh(),qe=po(Qn,je),Le=Pe(L),mt=Sh();e=H(e,"modal");const mo=y.useMemo(()=>({onHide:Le}),[Le]);function ln(){return Qe||Wv({isRTL:mt})}function on($){if(!Wn)return;const un=ln().getScrollbarWidth()>0,Zu=$.scrollHeight>fo($).documentElement.clientHeight;Ke({paddingRight:un&&!Zu?pa():void 0,paddingLeft:!un&&Zu?pa():void 0})}const Kn=Pe(()=>{fe&&on(fe.dialog)});bf(()=>{Qi(window,"resize",Kn),A.current==null||A.current()});const Br=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{R(!0),A.current=Wf(fe.dialog,()=>{R(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}L==null||L()},Yn=$=>{g?w==null||w($):($.preventDefault(),p==="static"&&It())},$d=($,un)=>{$&&on($),U==null||U($,un)},Dd=$=>{A.current==null||A.current(),T==null||T($)},Id=($,un)=>{P==null||P($,un),Hf(window,"resize",Kn)},Ad=$=>{$&&($.style.display=""),ie==null||ie($),Qi(window,"resize",Kn)},Ud=y.useCallback($=>v.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!m&&"show")}),[m,Ve,e]),Xu={...n,..._e};Xu.display="block";const Bd=$=>v.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,x&&`${e}-static`,!m&&"show"),onClick:p?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:v.jsx(i,{...rn,onMouseDown:Br,className:r,contentClassName:l,children:o})});return v.jsx(Sd.Provider,{value:mo,children:v.jsx(Iv,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:h,restoreFocus:S,restoreFocusOptions:C,onEscapeKeyDown:Yn,onShow:k,onHide:L,onEnter:$d,onEntering:Id,onEntered:N,onExit:Dd,onExiting:j,onExited:Ad,manager:ln(),transition:m?qv:void 0,backdropTransition:m?bv:void 0,renderBackdrop:Ud,renderDialog:Bd})})});Td.displayName="Modal";const tt=Object.assign(Td,{Body:Vv,Header:Xv,Title:Jv,Footer:Kv,Dialog:Ed,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ey(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...m},p){return v.jsx("div",{ref:p,...m,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${ey(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?v.jsx("span",{className:"visually-hidden",children:r}):r})}const _d=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p,className:g,children:w,...k}=r;return v.jsx("div",{ref:n,...k,className:M(g,m),children:w?av(w,L=>y.cloneElement(L,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p},n)})});_d.displayName="ProgressBar";const ty=_d,jd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=$f(),u=Df(),s=`${o}-cols`,a=[];return i.forEach(d=>{const m=r[d];delete r[d];let p;m!=null&&typeof m=="object"?{cols:p}=m:p=m;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),v.jsx(n,{ref:l,...r,className:M(t,o,...a)})});jd.displayName="Row";const Ld=jd,Rd=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return v.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Rd.displayName="Spinner";const An=Rd,ny=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",Gi="initializing",ry="paused",Od="live",vt=async(e,t,n)=>{console.log(e,t);const r=ny+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ft={listTorrents:()=>vt("GET","/torrents"),getTorrentDetails:e=>vt("GET",`/torrents/${e}`),getTorrentStats:e=>vt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),vt("POST",n,e)},pause:e=>vt("POST",`/torrents/${e}/pause`),start:e=>vt("POST",`/torrents/${e}/start`),forget:e=>vt("POST",`/torrents/${e}/forget`),delete:e=>vt("POST",`/torrents/${e}/delete`)},Vn=y.createContext(null),Pd=y.createContext(null),Ko=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return v.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},ly=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Vn),d=()=>{l(!1),i(null),s(!1),n()},m=()=>{s(!0),(r?ft.delete:ft.forget)(e).then(()=>{a.refreshTorrents(),d()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return v.jsxs(tt,{show:t,onHide:d,children:[v.jsx(tt.Header,{closeButton:!0,children:"Delete torrent"}),v.jsxs(tt.Body,{children:[v.jsx(On,{children:v.jsx(On.Group,{controlId:"delete-torrent",children:v.jsx(On.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&v.jsx(zr,{error:o})]}),v.jsxs(tt.Footer,{children:[u&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:m,disabled:u,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:d,children:"Cancel"})]})]})},oy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Pd);const s=n=="live",a=n=="paused",d=y.useContext(Vn),m=()=>{l(!0),ft.start(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error starting torrent id=${e}`,details:k})}).finally(()=>l(!1))},p=()=>{l(!0),ft.pause(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error pausing torrent id=${e}`,details:k})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},w=()=>{l(!1),i(!1)};return v.jsx(Ld,{children:v.jsxs(Vu,{children:[a&&v.jsx(Ko,{className:"bi-play-circle",onClick:m,disabled:r,color:"success"}),s&&v.jsx(Ko,{className:"bi-pause-circle",onClick:p,disabled:r}),v.jsx(Ko,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),v.jsx(ly,{id:e,show:o,onHide:w})]})})},iy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Gi||r==Od)&&!u,d=l?"Error":`${s.toFixed(2)}%`,m=l?"danger":u?"success":"primary",p=()=>{var L;let k=(L=n==null?void 0:n.live)==null?void 0:L.snapshot.peer_stats;return k?`${k.live} / ${k.seen}`:""},g=()=>{var k;return u?"Completed":r==Gi?"Checking files":((k=n.live)==null?void 0:k.download_speed.human_readable)??"N/A"};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),v.jsxs(Ld,{className:w.join(" "),children:[v.jsx(yt,{size:3,label:"Name",children:t?v.jsxs(v.Fragment,{children:[v.jsx("div",{className:"text-truncate",children:vy(t)}),l&&v.jsxs("p",{className:"text-danger",children:[v.jsx("strong",{children:"Error:"})," ",l]})]}):v.jsx(An,{})}),n?v.jsxs(v.Fragment,{children:[v.jsx(yt,{label:"Size",children:`${Md(o)} `}),v.jsx(yt,{size:2,label:r==ry?"Progress (PAUSED)":"Progress",children:v.jsx(ty,{now:s,label:d,animated:a,variant:m})}),v.jsx(yt,{size:2,label:"Down Speed",children:g()}),v.jsx(yt,{label:"ETA",children:yy(n)}),v.jsx(yt,{size:2,label:"Peers",children:p()}),v.jsx(yt,{label:"Actions",children:v.jsx(oy,{id:e,statsResponse:n})})]}):v.jsx(yt,{label:"Loading stats",size:8,children:v.jsx(An,{})})]})},yt=({size:e,label:t,children:n})=>v.jsxs(Vu,{md:e||1,className:"py-3",children:[v.jsx("div",{className:"fw-bold",children:t}),n]}),uy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null);y.useEffect(()=>{if(n===null)return wy(async()=>{await ft.getTorrentDetails(t.id).then(r)},1e3)},[n]);const i=()=>ft.getTorrentStats(t.id).then(u=>(o(u),u));return y.useEffect(()=>zd(async()=>i().then(m=>m.finished?1e4:m.state==Gi||m.state==Od?1e3:1e4,m=>1e4),0),[]),v.jsx(Pd.Provider,{value:{refresh:i},children:v.jsx(iy,{id:e,detailsResponse:n,statsResponse:l})})},sy=e=>{if(e.torrents===null&&e.loading)return v.jsx(An,{});if(e.torrents!==null)return e.torrents.length===0?v.jsx("div",{className:"text-center",children:v.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):v.jsx(v.Fragment,{children:e.torrents.map(t=>v.jsx(uy,{id:t.id,torrent:t},t.id))})},ay=()=>{const[e,t]=y.useState(null),[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(!1),s=async()=>{u(!0);let d=await ft.listTorrents().finally(()=>u(!1));o(d.torrents)};y.useEffect(()=>zd(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return v.jsx(Vn.Provider,{value:a,children:v.jsxs("div",{className:"text-center",children:[v.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 0.0.1-alpha"}),v.jsx(hy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},cy=e=>{let{details:t}=e;return t?v.jsxs(v.Fragment,{children:[t.status&&v.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},zr=e=>{let{error:t,remove:n}=e;return t==null?null:v.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[v.jsx(fa.Heading,{children:t.text}),v.jsx(cy,{details:t.details})]})},Fd=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState([]),[a,d]=y.useState(null);y.useContext(Vn);const m=n!==null||a!==null;y.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const w=await ft.uploadTorrent(n,{listOnly:!0});s(w.details.files)}catch(w){d({text:"Error listing torrent",details:w})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return v.jsxs(v.Fragment,{children:[v.jsx(Mr,{variant:l,onClick:t,className:"m-1",children:e}),v.jsx(py,{show:m,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},fy=()=>{let[e,t]=y.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return v.jsx(Fd,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},dy=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return v.jsxs(v.Fragment,{children:[v.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),v.jsx(Fd,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},py=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=y.useState([]),[a,d]=y.useState(!1),[m,p]=y.useState(null),g=y.useContext(Vn);y.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const w=()=>{n(),s([]),p(null),d(!1)},k=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},L=async()=>{d(!0),ft.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return v.jsxs(tt,{show:t,onHide:w,size:"lg",children:[v.jsx(tt.Header,{closeButton:!0,children:!!l||v.jsx(tt.Title,{children:"Select Files"})}),v.jsxs(tt.Body,{children:[o?v.jsx(An,{}):l?v.jsx(zr,{error:l}):v.jsx(On,{children:r.map((f,c)=>v.jsx(On.Group,{controlId:`check-${c}`,children:v.jsx(On.Check,{type:"checkbox",label:`${f.name} (${Md(f.length)})`,checked:u.includes(c),onChange:()=>k(c)})},c))}),v.jsx(zr,{error:m})]}),v.jsxs(tt.Footer,{children:[a&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:L,disabled:o||a||u.length==0,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:w,children:"Cancel"})]})]})},my=()=>v.jsxs("div",{id:"buttons-container",className:"mt-3",children:[v.jsx(fy,{}),v.jsx(dy,{})]}),hy=e=>{let t=y.useContext(Vn);return v.jsxs(dv,{children:[v.jsx(zr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),v.jsx(zr,{error:e.otherError}),v.jsx(sy,{torrents:e.torrents,loading:e.torrentsLoading}),v.jsx(my,{})]})};function Md(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function vy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function yy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":gy(t)}function gy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function zd(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function wy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function Sy(){const e=document.getElementById("app");Go.createRoot(e).render(v.jsx(y.StrictMode,{children:v.jsx(ay,{})}))}document.addEventListener("DOMContentLoaded",Sy); +*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function ph(e){var t=mh(e,"string");return typeof t=="symbol"?t:String(t)}function mh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function hh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var d=arguments.length,m=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Ah(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Uh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function po(e,t){return y.useMemo(()=>Uh(e,t),[e,t])}function Bh(e){return e&&"setState"in e?Cn.findDOMNode(e):e??null}const Hh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const m=y.useRef(null),p=po(m,s),g=N=>{p(Bh(N))},w=N=>T=>{N&&m.current&&N(m.current,T)},k=y.useCallback(w(e),[e]),R=y.useCallback(w(t),[t]),f=y.useCallback(w(n),[n]),c=y.useCallback(w(r),[r]),h=y.useCallback(w(l),[l]),S=y.useCallback(w(o),[o]),C=y.useCallback(w(i),[i]);return v.jsx(Mh,{ref:d,...a,onEnter:k,onEntered:f,onEntering:R,onExit:c,onExited:S,onExiting:h,addEndListener:C,nodeRef:m,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Wt.cloneElement(u,{ref:g})})}),Wh=Hh;function Vh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Vh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Vf=e=>y.forwardRef((t,n)=>v.jsx("div",{...t,ref:n,className:M(t.className,e)})),Qf=Vf("h4");Qf.displayName="DivStyledAsH4";const Kf=y.forwardRef(({className:e,bsPrefix:t,as:n=Qf,...r},l)=>(t=H(t,"alert-heading"),v.jsx(n,{ref:l,className:M(e,t),...r})));Kf.displayName="AlertHeading";const Qh=Kf;function Kh(){return y.useState(null)}function Gh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Yh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Xh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Zh=typeof document<"u",ca=Zh||Xh?y.useLayoutEffect:y.useEffect,Jh=["as","disabled"];function qh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function bh(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&bh(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},m=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:m},a]}const ev=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=qh(e,Jh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return v.jsx(i,Object.assign({},l,o,{ref:t}))});ev.displayName="Button";const tv=["onKeyDown"];function nv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function rv(e){return!e||e.trim()==="#"}const Gf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=nv(e,tv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return rv(r.href)||r.role==="button"?v.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):v.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Gf.displayName="Anchor";const lv=Gf,Yf=y.forwardRef(({className:e,bsPrefix:t,as:n=lv,...r},l)=>(t=H(t,"alert-link"),v.jsx(n,{ref:l,className:M(e,t),...r})));Yf.displayName="AlertLink";const ov=Yf,iv={[St]:"show",[Ht]:"show"},Xf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Ah(s),r==null||r(s,a)},[r]);return v.jsx(Wh,{ref:o,addEndListener:Ih,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,iv[s],n[s])})})});Xf.displayName="Fade";const Ql=Xf,uv={"aria-label":ot.string,onClick:ot.func,variant:ot.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>v.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=uv;const Zf=Wu,Jf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:m=Ql,...p}=vh(e,{show:"onClose"}),g=H(n,"alert"),w=Pe(f=>{a&&a(!1,f)}),k=m===!0?Ql:m,R=v.jsxs("div",{role:"alert",...k?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&v.jsx(Zf,{onClick:w,"aria-label":l,variant:o}),u]});return k?v.jsx(k,{unmountOnExit:!0,...p,ref:void 0,in:r,children:R}):r?R:null});Jf.displayName="Alert";const fa=Object.assign(Jf,{Link:ov,Heading:Qh}),qf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:m}]=Hu({tagName:e,disabled:o,...u}),p=m;return v.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});qf.displayName="Button";const Mr=qf;function sv(e){const t=y.useRef(e);return t.current=e,t}function bf(e){const t=sv(e);y.useEffect(()=>()=>t.current(),[])}function av(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function cv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function fv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=$f(),o=Df(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,m,p;typeof a=="object"&&a!=null?{span:d,offset:m,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),m!=null&&u.push(`offset${g}-${m}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const ed=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=fv(e);return v.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});ed.displayName="Col";const Vu=ed,td=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return v.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});td.displayName="Container";const dv=td;var pv=Function.prototype.bind.call(Function.prototype.call,[].slice);function cn(e,t){return pv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const mv="data-rr-ui-";function hv(e){return`${mv}${e}`}const nd=y.createContext(Wn?window:void 0);nd.Provider;function Qu(){return y.useContext(nd)}const vv={type:ot.string,tooltip:ot.bool,as:ot.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>v.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=vv;const rd=Ku,yv=y.createContext({}),ct=yv,ld=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ct);return t=H(t,"form-check-input"),v.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});ld.displayName="FormCheckInput";const od=ld,id=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-check-label"),v.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});id.displayName="FormCheckLabel";const Ki=id,ud=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:m,style:p,title:g="",type:w="checkbox",label:k,children:R,as:f="input",...c},h)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ct),C=y.useMemo(()=>({controlId:e||S}),[S,e]),N=!R&&k!=null&&k!==!1||cv(R,Ki),T=v.jsx(od,{...c,type:w==="switch"?"checkbox":w,ref:h,isValid:i,isInvalid:u,disabled:o,as:f});return v.jsx(ct.Provider,{value:C,children:v.jsx("div",{style:p,className:M(m,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,w==="switch"&&n),children:R||v.jsxs(v.Fragment,{children:[T,N&&v.jsx(Ki,{title:g,children:k}),a&&v.jsx(rd,{type:d,tooltip:s,children:a})]})})})});ud.displayName="FormCheck";const Kl=Object.assign(ud,{Input:od,Label:Ki}),sd=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...m},p)=>{const{controlId:g}=y.useContext(ct);return e=H(e,"form-control"),v.jsx(d,{...m,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});sd.displayName="FormControl";const gv=Object.assign(sd,{Feedback:rd}),ad=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),v.jsx(n,{ref:l,className:M(e,t),...r})));ad.displayName="FormFloating";const wv=ad,cd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return v.jsx(ct.Provider,{value:l,children:v.jsx(t,{...n,ref:r})})});cd.displayName="FormGroup";const fd=cd,dd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ct);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?v.jsx(Vu,{ref:u,as:"label",className:d,htmlFor:o,...i}):v.jsx(e,{ref:u,className:d,htmlFor:o,...i})});dd.displayName="FormLabel";const Sv=dd,pd=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-range"),v.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});pd.displayName="FormRange";const kv=pd,md=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ct);return e=H(e,"form-select"),v.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});md.displayName="FormSelect";const Ev=md,hd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),v.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));hd.displayName="FormText";const xv=hd,vd=y.forwardRef((e,t)=>v.jsx(Kl,{...e,ref:t,type:"switch"}));vd.displayName="Switch";const Cv=Object.assign(vd,{Input:Kl.Input,Label:Kl.Label}),yd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),v.jsxs(fd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,v.jsx("label",{htmlFor:r,children:l})]})));yd.displayName="FloatingLabel";const Nv=yd,Tv={_ref:ot.any,validated:ot.bool,as:ot.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>v.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=Tv;const On=Object.assign(Gu,{Group:fd,Control:gv,Floating:wv,Check:Kl,Switch:Cv,Label:Sv,Text:xv,Range:kv,Select:Ev,FloatingLabel:Nv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Wn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Ho(e){e===void 0&&(e=fo());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function _v(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=hv("modal-open");class jv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return _v(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=jv,Wo=(e,t)=>Wn?e==null?(t||fo()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Rv(e,t){const n=Qu(),[r,l]=y.useState(()=>Wo(e,n==null?void 0:n.document));if(!r){const o=Wo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Wo(e);o!==r&&l(o)},[e,r]),r}function Lv({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Pe(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=po(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Ov({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Pe(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Pv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Ov({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,m=>{throw a.in||i(!0),m})}}),s=po(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?v.jsx(e,Object.assign({},n)):t?v.jsx(Pv,Object.assign({},n,{transition:t})):v.jsx(Lv,Object.assign({},n))}function Fv(e){return e.code==="Escape"||e.keyCode===27}const Mv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function zv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Vo;function $v(e){return Vo||(Vo=new Yu({ownerDocument:e==null?void 0:e.document})),Vo}function Dv(e){const t=Qu(),n=e||$v(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const gd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:m,runTransition:p,backdropTransition:g,runBackdropTransition:w,autoFocus:k=!0,enforceFocus:R=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:h,renderBackdrop:S=K=>v.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:j=()=>{},onExit:U,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:rn}=e,Qn=zv(e,Mv);const _e=Qu(),Ke=Rv(N),x=Dv(C),L=Gh(),O=Yh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>x,[x]),Wn&&!O&&n&&(fe.current=Ho(_e==null?void 0:_e.document)),n&&D&&A(!1);const je=Pe(()=>{if(x.add(),on.current=Vl(document,"keydown",mo),ln.current=Vl(document,"focus",()=>setTimeout(Re),!0),T&&T(),k){var K,Hr;const Yn=Ho((K=(Hr=x.dialog)==null?void 0:Hr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Yn&&!da(x.dialog,Yn)&&(fe.current=Yn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),on.current==null||on.current(),ln.current==null||ln.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ke||je()},[n,Ke,je]),y.useEffect(()=>{D&&qe()},[D,qe]),bf(()=>{qe()});const Re=Pe(()=>{if(!R||!L()||!x.isTopModal())return;const K=Ho(_e==null?void 0:_e.document);x.dialog&&K&&!da(x.dialog,K)&&x.dialog.focus()}),mt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),mo=Pe(K=>{s&&Fv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||j())}),ln=y.useRef(),on=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const Br=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=h?h(Br):v.jsx("div",Object.assign({},Br,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(m,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:U,onExiting:ie,onExited:Kn,onEnter:Ve,onEntering:Qe,onEntered:rn,children:Gn});let It=null;return u&&(It=S({ref:x.setBackdropRef,onClick:mt}),It=ha(g,w,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),v.jsx(v.Fragment,{children:Cn.createPortal(v.jsxs(v.Fragment,{children:[It,Gn]}),Ke)})});gd.displayName="Modal";const Iv=Object.assign(gd,{Manager:Yu});function Av(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Uv(e,t){e.classList?e.classList.add(t):Av(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Bv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const fn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Hv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Uv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Bv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Qo;function Wv(e){return Qo||(Qo=new Hv(e)),Qo}const wd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),v.jsx(n,{ref:l,className:M(e,t),...r})));wd.displayName="ModalBody";const Vv=wd,Qv=y.createContext({onHide(){}}),Sd=Qv,kd=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,m=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return v.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&m),children:v.jsx("div",{className:M(`${e}-content`,n),children:i})})});kd.displayName="ModalDialog";const Ed=kd,xd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),v.jsx(n,{ref:l,className:M(e,t),...r})));xd.displayName="ModalFooter";const Kv=xd,Gv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(Sd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return v.jsxs("div",{ref:i,...o,children:[l,n&&v.jsx(Zf,{"aria-label":e,variant:t,onClick:s})]})}),Yv=Gv,Cd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),v.jsx(Yv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Cd.displayName="ModalHeader";const Xv=Cd,Zv=Vf("h4"),Nd=y.forwardRef(({className:e,bsPrefix:t,as:n=Zv,...r},l)=>(t=H(t,"modal-title"),v.jsx(n,{ref:l,className:M(e,t),...r})));Nd.displayName="ModalTitle";const Jv=Nd;function qv(e){return v.jsx(Ql,{...e,timeout:null})}function bv(e){return v.jsx(Ql,{...e,timeout:null})}const Td=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=Ed,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:m=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:w,onShow:k,onHide:R,container:f,autoFocus:c=!0,enforceFocus:h=!0,restoreFocus:S=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:j,onEnter:U,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...rn},Qn)=>{const[_e,Ke]=y.useState({}),[x,L]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,je]=Kh(),qe=po(Qn,je),Re=Pe(R),mt=Sh();e=H(e,"modal");const mo=y.useMemo(()=>({onHide:Re}),[Re]);function ln(){return Qe||Wv({isRTL:mt})}function on($){if(!Wn)return;const un=ln().getScrollbarWidth()>0,Zu=$.scrollHeight>fo($).documentElement.clientHeight;Ke({paddingRight:un&&!Zu?pa():void 0,paddingLeft:!un&&Zu?pa():void 0})}const Kn=Pe(()=>{fe&&on(fe.dialog)});bf(()=>{Qi(window,"resize",Kn),A.current==null||A.current()});const Br=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{L(!0),A.current=Wf(fe.dialog,()=>{L(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}R==null||R()},Yn=$=>{g?w==null||w($):($.preventDefault(),p==="static"&&It())},$d=($,un)=>{$&&on($),U==null||U($,un)},Dd=$=>{A.current==null||A.current(),T==null||T($)},Id=($,un)=>{P==null||P($,un),Hf(window,"resize",Kn)},Ad=$=>{$&&($.style.display=""),ie==null||ie($),Qi(window,"resize",Kn)},Ud=y.useCallback($=>v.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!m&&"show")}),[m,Ve,e]),Xu={...n,..._e};Xu.display="block";const Bd=$=>v.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,x&&`${e}-static`,!m&&"show"),onClick:p?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:v.jsx(i,{...rn,onMouseDown:Br,className:r,contentClassName:l,children:o})});return v.jsx(Sd.Provider,{value:mo,children:v.jsx(Iv,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:h,restoreFocus:S,restoreFocusOptions:C,onEscapeKeyDown:Yn,onShow:k,onHide:R,onEnter:$d,onEntering:Id,onEntered:N,onExit:Dd,onExiting:j,onExited:Ad,manager:ln(),transition:m?qv:void 0,backdropTransition:m?bv:void 0,renderBackdrop:Ud,renderDialog:Bd})})});Td.displayName="Modal";const tt=Object.assign(Td,{Body:Vv,Header:Xv,Title:Jv,Footer:Kv,Dialog:Ed,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ey(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...m},p){return v.jsx("div",{ref:p,...m,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${ey(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?v.jsx("span",{className:"visually-hidden",children:r}):r})}const _d=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p,className:g,children:w,...k}=r;return v.jsx("div",{ref:n,...k,className:M(g,m),children:w?av(w,R=>y.cloneElement(R,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p},n)})});_d.displayName="ProgressBar";const ty=_d,jd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=$f(),u=Df(),s=`${o}-cols`,a=[];return i.forEach(d=>{const m=r[d];delete r[d];let p;m!=null&&typeof m=="object"?{cols:p}=m:p=m;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),v.jsx(n,{ref:l,...r,className:M(t,o,...a)})});jd.displayName="Row";const Rd=jd,Ld=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return v.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Ld.displayName="Spinner";const An=Ld,ny=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",Gi="initializing",ry="paused",Od="live",vt=async(e,t,n)=>{console.log(e,t);const r=ny+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ft={listTorrents:()=>vt("GET","/torrents"),getTorrentDetails:e=>vt("GET",`/torrents/${e}`),getTorrentStats:e=>vt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),vt("POST",n,e)},pause:e=>vt("POST",`/torrents/${e}/pause`),start:e=>vt("POST",`/torrents/${e}/start`),forget:e=>vt("POST",`/torrents/${e}/forget`),delete:e=>vt("POST",`/torrents/${e}/delete`)},Vn=y.createContext(null),Pd=y.createContext(null),Ko=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return v.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},ly=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Vn),d=()=>{l(!1),i(null),s(!1),n()},m=()=>{s(!0),(r?ft.delete:ft.forget)(e).then(()=>{a.refreshTorrents(),d()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return v.jsxs(tt,{show:t,onHide:d,children:[v.jsx(tt.Header,{closeButton:!0,children:"Delete torrent"}),v.jsxs(tt.Body,{children:[v.jsx(On,{children:v.jsx(On.Group,{controlId:"delete-torrent",children:v.jsx(On.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&v.jsx(zr,{error:o})]}),v.jsxs(tt.Footer,{children:[u&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:m,disabled:u,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:d,children:"Cancel"})]})]})},oy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Pd);const s=n=="live",a=n=="paused",d=y.useContext(Vn),m=()=>{l(!0),ft.start(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error starting torrent id=${e}`,details:k})}).finally(()=>l(!1))},p=()=>{l(!0),ft.pause(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error pausing torrent id=${e}`,details:k})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},w=()=>{l(!1),i(!1)};return v.jsx(Rd,{children:v.jsxs(Vu,{children:[a&&v.jsx(Ko,{className:"bi-play-circle",onClick:m,disabled:r,color:"success"}),s&&v.jsx(Ko,{className:"bi-pause-circle",onClick:p,disabled:r}),v.jsx(Ko,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),v.jsx(ly,{id:e,show:o,onHide:w})]})})},iy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Gi||r==Od)&&!u,d=l?"Error":`${s.toFixed(2)}%`,m=l?"danger":u?"success":"primary",p=()=>{var R;let k=(R=n==null?void 0:n.live)==null?void 0:R.snapshot.peer_stats;return k?`${k.live} / ${k.seen}`:""},g=()=>{var k;return u?"Completed":r==Gi?"Checking files":((k=n.live)==null?void 0:k.download_speed.human_readable)??"N/A"};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),v.jsxs(Rd,{className:w.join(" "),children:[v.jsx(yt,{size:3,label:"Name",children:t?v.jsxs(v.Fragment,{children:[v.jsx("div",{className:"text-truncate",children:vy(t)}),l&&v.jsxs("p",{className:"text-danger",children:[v.jsx("strong",{children:"Error:"})," ",l]})]}):v.jsx(An,{})}),n?v.jsxs(v.Fragment,{children:[v.jsx(yt,{label:"Size",children:`${Md(o)} `}),v.jsx(yt,{size:2,label:r==ry?"Progress (PAUSED)":"Progress",children:v.jsx(ty,{now:s,label:d,animated:a,variant:m})}),v.jsx(yt,{size:2,label:"Down Speed",children:g()}),v.jsx(yt,{label:"ETA",children:yy(n)}),v.jsx(yt,{size:2,label:"Peers",children:p()}),v.jsx(yt,{label:"Actions",children:v.jsx(oy,{id:e,statsResponse:n})})]}):v.jsx(yt,{label:"Loading stats",size:8,children:v.jsx(An,{})})]})},yt=({size:e,label:t,children:n})=>v.jsxs(Vu,{md:e||1,className:"py-3",children:[v.jsx("div",{className:"fw-bold",children:t}),n]}),uy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(0),s=()=>{u(i+1)};return y.useEffect(()=>{if(n===null)return wy(async()=>{await ft.getTorrentDetails(t.id).then(r)},1e3)},[n]),y.useEffect(()=>zd(async()=>ft.getTorrentStats(t.id).then(g=>(o(g),g)).then(g=>g.finished?1e4:g.state==Gi||g.state==Od?1e3:1e4,g=>1e4),0),[i]),v.jsx(Pd.Provider,{value:{refresh:s},children:v.jsx(iy,{id:e,detailsResponse:n,statsResponse:l})})},sy=e=>{if(e.torrents===null&&e.loading)return v.jsx(An,{});if(e.torrents!==null)return e.torrents.length===0?v.jsx("div",{className:"text-center",children:v.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):v.jsx(v.Fragment,{children:e.torrents.map(t=>v.jsx(uy,{id:t.id,torrent:t},t.id))})},ay=()=>{const[e,t]=y.useState(null),[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(!1),s=async()=>{u(!0);let d=await ft.listTorrents().finally(()=>u(!1));o(d.torrents)};y.useEffect(()=>zd(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return v.jsx(Vn.Provider,{value:a,children:v.jsxs("div",{className:"text-center",children:[v.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 0.0.1-alpha"}),v.jsx(hy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},cy=e=>{let{details:t}=e;return t?v.jsxs(v.Fragment,{children:[t.status&&v.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},zr=e=>{let{error:t,remove:n}=e;return t==null?null:v.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[v.jsx(fa.Heading,{children:t.text}),v.jsx(cy,{details:t.details})]})},Fd=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState([]),[a,d]=y.useState(null);y.useContext(Vn);const m=n!==null||a!==null;y.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const w=await ft.uploadTorrent(n,{listOnly:!0});s(w.details.files)}catch(w){d({text:"Error listing torrent",details:w})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return v.jsxs(v.Fragment,{children:[v.jsx(Mr,{variant:l,onClick:t,className:"m-1",children:e}),v.jsx(py,{show:m,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},fy=()=>{let[e,t]=y.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return v.jsx(Fd,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},dy=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return v.jsxs(v.Fragment,{children:[v.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),v.jsx(Fd,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},py=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=y.useState([]),[a,d]=y.useState(!1),[m,p]=y.useState(null),g=y.useContext(Vn);y.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const w=()=>{n(),s([]),p(null),d(!1)},k=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},R=async()=>{d(!0),ft.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return v.jsxs(tt,{show:t,onHide:w,size:"lg",children:[v.jsx(tt.Header,{closeButton:!0,children:!!l||v.jsx(tt.Title,{children:"Select Files"})}),v.jsxs(tt.Body,{children:[o?v.jsx(An,{}):l?v.jsx(zr,{error:l}):v.jsx(On,{children:r.map((f,c)=>v.jsx(On.Group,{controlId:`check-${c}`,children:v.jsx(On.Check,{type:"checkbox",label:`${f.name} (${Md(f.length)})`,checked:u.includes(c),onChange:()=>k(c)})},c))}),v.jsx(zr,{error:m})]}),v.jsxs(tt.Footer,{children:[a&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:R,disabled:o||a||u.length==0,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:w,children:"Cancel"})]})]})},my=()=>v.jsxs("div",{id:"buttons-container",className:"mt-3",children:[v.jsx(fy,{}),v.jsx(dy,{})]}),hy=e=>{let t=y.useContext(Vn);return v.jsxs(dv,{children:[v.jsx(zr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),v.jsx(zr,{error:e.otherError}),v.jsx(sy,{torrents:e.torrents,loading:e.torrentsLoading}),v.jsx(my,{})]})};function Md(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function vy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function yy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":gy(t)}function gy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function zd(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function wy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function Sy(){const e=document.getElementById("app");Go.createRoot(e).render(v.jsx(y.StrictMode,{children:v.jsx(ay,{})}))}document.addEventListener("DOMContentLoaded",Sy); diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 5edab1a..4eed9db 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -241,6 +241,11 @@ const Column: React.FC<{ const Torrent = ({ id, torrent }) => { const [detailsResponse, updateDetailsResponse] = useState(null); const [statsResponse, updateStatsResponse] = useState(null); + const [forceStatsRefresh, setForceStatsRefresh] = useState(0); + + const forceStatsRefreshCallback = () => { + setForceStatsRefresh(forceStatsRefresh + 1); + } // Update details once. useEffect(() => { @@ -251,11 +256,6 @@ const Torrent = ({ id, torrent }) => { } }, [detailsResponse]); - const refreshStats = () => API.getTorrentStats(torrent.id).then((stats) => { - updateStatsResponse(stats); - return stats; - }); - // Update stats once then forever. useEffect(() => customSetInterval((async () => { const errorInterval = 10000; @@ -263,7 +263,10 @@ const Torrent = ({ id, torrent }) => { const finishedInterval = 10000; const nonLiveInterval = 10000; - return refreshStats().then((stats) => { + return API.getTorrentStats(torrent.id).then((stats) => { + updateStatsResponse(stats); + return stats; + }).then((stats) => { if (stats.finished) { return finishedInterval; } @@ -274,9 +277,9 @@ const Torrent = ({ id, torrent }) => { }, (e) => { return errorInterval; }); - }), 0), []); + }), 0), [forceStatsRefresh]); - return + return } From bec5e1be7faf8b228eda8723a268e09a6668ed76 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 02:36:19 +0000 Subject: [PATCH 26/38] Session persistence --- .cargo/config | 5 +- crates/librqbit/src/session.rs | 178 ++++++++++++++++++++--- crates/librqbit/src/spawn_utils.rs | 4 +- crates/librqbit/src/torrent_state/mod.rs | 8 +- crates/librqbit_core/src/magnet.rs | 11 ++ crates/rqbit/Cargo.toml | 4 +- crates/rqbit/src/main.rs | 45 +++--- 7 files changed, 204 insertions(+), 51 deletions(-) diff --git a/.cargo/config b/.cargo/config index 1b76467..0aca9d9 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,5 +1,2 @@ [target.arm-unknown-linux-gnueabihf] -rustflags = ["-l", "atomic"] - -[build] -rustflags = ["--cfg", "tokio_unstable"] \ No newline at end of file +rustflags = ["-l", "atomic"] \ No newline at end of file diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 6733eb9..ddf82dc 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -1,5 +1,13 @@ use std::{ - borrow::Cow, collections::HashMap, io::Read, net::SocketAddr, path::PathBuf, time::Duration, + borrow::Cow, + collections::{HashMap, HashSet}, + fs::{File, OpenOptions}, + io::{BufReader, BufWriter, Read}, + net::SocketAddr, + path::PathBuf, + str::FromStr, + sync::Arc, + time::Duration, }; use anyhow::{bail, Context}; @@ -12,13 +20,14 @@ use librqbit_core::{ }; use parking_lot::RwLock; use reqwest::Url; +use serde::{Deserialize, Serialize}; use tokio_stream::StreamExt; -use tracing::{debug, error_span, info, warn}; +use tracing::{debug, error, error_span, info, warn}; use crate::{ dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult}, peer_connection::PeerConnectionOptions, - spawn_utils::BlockingSpawner, + spawn_utils::{spawn, BlockingSpawner}, torrent_state::{ManagedTorrentBuilder, ManagedTorrentHandle, ManagedTorrentState}, }; @@ -27,26 +36,62 @@ pub const SUPPORTED_SCHEMES: [&str; 3] = ["http:", "https:", "magnet:"]; pub type TorrentId = usize; #[derive(Default)] -pub struct SessionLocked { +pub struct SessionDatabase { next_id: usize, torrents: HashMap, } -impl SessionLocked { +impl SessionDatabase { fn add_torrent(&mut self, torrent: ManagedTorrentHandle) -> TorrentId { let idx = self.next_id; self.torrents.insert(idx, torrent); self.next_id += 1; idx } + + fn serialize(&self) -> SerializedSessionDatabase { + SerializedSessionDatabase { + torrents: self + .torrents + .values() + .map(|torrent| SerializedTorrent { + trackers: torrent + .info() + .trackers + .iter() + .map(|u| u.to_string()) + .collect(), + info_hash: torrent.info_hash().as_string(), + only_files: torrent.only_files.clone(), + is_paused: torrent.with_state(|s| matches!(s, ManagedTorrentState::Paused(_))), + output_folder: torrent.info().out_dir.clone(), + }) + .collect(), + } + } +} + +#[derive(Serialize, Deserialize)] +struct SerializedTorrent { + info_hash: String, + trackers: HashSet, + output_folder: PathBuf, + only_files: Option>, + is_paused: bool, +} + +#[derive(Serialize, Deserialize)] +struct SerializedSessionDatabase { + torrents: Vec, } pub struct Session { peer_id: Id20, dht: Option, + persistence_filename: PathBuf, peer_opts: PeerConnectionOptions, spawner: BlockingSpawner, - locked: RwLock, + db: RwLock, output_folder: PathBuf, } @@ -86,6 +131,7 @@ fn compute_only_files>( #[derive(Default, Clone)] pub struct AddTorrentOptions { + pub paused: bool, pub only_files_regex: Option, pub only_files: Option>, pub overwrite: bool, @@ -164,20 +210,24 @@ impl<'a> AddTorrent<'a> { pub struct SessionOptions { pub disable_dht: bool, pub disable_dht_persistence: bool, + pub persistence: bool, pub dht_config: Option, pub peer_id: Option, pub peer_opts: Option, } impl Session { - pub async fn new(output_folder: PathBuf, spawner: BlockingSpawner) -> anyhow::Result { + pub async fn new( + output_folder: PathBuf, + spawner: BlockingSpawner, + ) -> anyhow::Result> { Self::new_with_opts(output_folder, spawner, SessionOptions::default()).await } pub async fn new_with_opts( output_folder: PathBuf, spawner: BlockingSpawner, opts: SessionOptions, - ) -> anyhow::Result { + ) -> anyhow::Result> { let peer_id = opts.peer_id.unwrap_or_else(generate_peer_id); let dht = if opts.disable_dht { None @@ -191,25 +241,117 @@ impl Session { Some(dht) }; let peer_opts = opts.peer_opts.unwrap_or_default(); - - Ok(Self { + let session_filename = output_folder.join(".rqbit-session.json"); + let session = Arc::new(Self { + persistence_filename: session_filename, peer_id, dht, peer_opts, spawner, output_folder, - locked: RwLock::new(SessionLocked::default()), - }) + db: RwLock::new(Default::default()), + }); + + if opts.persistence { + let session = session.clone(); + spawn( + "session persistene", + error_span!("session persistence"), + async move { + // Populate initial from the state filename + if let Err(e) = session.populate_from_stored().await { + error!("could not populate session from stored file: {:?}", e); + } + + let session = Arc::downgrade(&session); + + loop { + tokio::time::sleep(Duration::from_secs(10)).await; + let session = match session.upgrade() { + Some(s) => s, + None => break, + }; + if let Err(e) = session.dump_to_disk() { + error!("error dumping session to disk: {:?}", e); + } + } + + Ok(()) + }, + ); + } + + Ok(session) } pub fn get_dht(&self) -> Option<&Dht> { self.dht.as_ref() } + async fn populate_from_stored(&self) -> anyhow::Result<()> { + let mut rdr = BufReader::new( + std::fs::File::open(&self.persistence_filename).with_context(|| { + format!("error opening session file {:?}", self.persistence_filename) + })?, + ); + let db: SerializedSessionDatabase = + serde_json::from_reader(&mut rdr).context("error deserializing session database")?; + for storrent in db.torrents.into_iter() { + let magnet = Magnet { + info_hash: Id20::from_str(&storrent.info_hash) + .context("error deserializing info_hash")?, + trackers: storrent.trackers.into_iter().collect(), + }; + if let Err(e) = self + .add_torrent( + AddTorrent::Url(Cow::Owned(magnet.to_string())), + Some(AddTorrentOptions { + paused: storrent.is_paused, + output_folder: Some( + storrent + .output_folder + .to_str() + .context("broken path")? + .to_owned(), + ), + only_files: storrent.only_files, + overwrite: true, + ..Default::default() + }), + ) + .await + { + error!("error adding torrent from stored session: {:?}", e) + } + } + Ok(()) + } + + fn dump_to_disk(&self) -> anyhow::Result<()> { + let tmp_filename = format!("{}.tmp", self.persistence_filename.to_str().unwrap()); + let mut tmp = BufWriter::new( + std::fs::OpenOptions::new() + .create(true) + .create_new(true) + .truncate(true) + .write(true) + .open(&tmp_filename) + .with_context(|| format!("error opening {:?}", tmp_filename))?, + ); + let serialized = self.db.read().serialize(); + serde_json::to_writer(&mut tmp, &serialized).context("error serializing")?; + drop(tmp); + + std::fs::rename(&tmp_filename, &self.persistence_filename) + .context("error renaming persistence file")?; + debug!("wrote persistence to {:?}", &self.persistence_filename); + Ok(()) + } + pub fn with_torrents( &self, callback: impl Fn(&mut dyn Iterator) -> R, ) -> R { - callback(&mut self.locked.read().torrents.iter().map(|(id, t)| (*id, t))) + callback(&mut self.db.read().torrents.iter().map(|(id, t)| (*id, t))) } pub async fn add_torrent( @@ -407,7 +549,7 @@ impl Session { } let (managed_torrent, id) = { - let mut g = self.locked.write(); + let mut g = self.db.write(); if let Some((id, handle)) = g.torrents.iter().find(|(_, t)| t.info_hash() == info_hash) { return Ok(AddTorrentResponse::AlreadyManaged(*id, handle.clone())); @@ -422,7 +564,7 @@ impl Session { let span = managed_torrent.info.span.clone(); let _ = span.enter(); managed_torrent - .start(initial_peers, dht_peer_rx) + .start(initial_peers, dht_peer_rx, opts.paused) .context("error starting torrent")?; } @@ -430,12 +572,12 @@ impl Session { } pub fn get(&self, id: TorrentId) -> Option { - self.locked.read().torrents.get(&id).cloned() + self.db.read().torrents.get(&id).cloned() } pub fn delete(&self, id: TorrentId, delete_files: bool) -> anyhow::Result<()> { let removed = self - .locked + .db .write() .torrents .remove(&id) @@ -477,7 +619,7 @@ impl Session { .as_ref() .map(|dht| dht.get_peers(handle.info_hash())) .transpose()?; - handle.start(Default::default(), peer_rx)?; + handle.start(Default::default(), peer_rx, false)?; Ok(()) } } diff --git a/crates/librqbit/src/spawn_utils.rs b/crates/librqbit/src/spawn_utils.rs index 957a837..1e404bd 100644 --- a/crates/librqbit/src/spawn_utils.rs +++ b/crates/librqbit/src/spawn_utils.rs @@ -1,7 +1,7 @@ use tracing::{debug, trace, warn, Instrument}; pub fn spawn( - name: &str, + _name: &str, span: tracing::Span, fut: impl std::future::Future> + Send + 'static, ) -> tokio::task::JoinHandle<()> { @@ -17,7 +17,7 @@ pub fn spawn( } } .instrument(span.or_current()); - tokio::task::Builder::new().name(name).spawn(fut).unwrap() + tokio::task::spawn(fut) } #[derive(Clone, Copy, Debug)] diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 502a2dd..e95c5b3 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -85,7 +85,7 @@ pub struct ManagedTorrentInfo { pub struct ManagedTorrent { pub info: Arc, - only_files: Option>, + pub(crate) only_files: Option>, locked: RwLock, } @@ -138,6 +138,7 @@ impl ManagedTorrent { self: &Arc, initial_peers: Vec, peer_rx: Option + Unpin + Send + Sync + 'static>, + start_paused: bool, ) -> anyhow::Result<()> { let mut g = self.locked.write(); @@ -185,6 +186,11 @@ impl ManagedTorrent { return Ok(()); } + if start_paused { + g.state = ManagedTorrentState::Paused(paused); + return Ok(()); + } + let live = TorrentStateLive::new(paused); g.state = ManagedTorrentState::Live(live.clone()); diff --git a/crates/librqbit_core/src/magnet.rs b/crates/librqbit_core/src/magnet.rs index 5b5739e..12e09d9 100644 --- a/crates/librqbit_core/src/magnet.rs +++ b/crates/librqbit_core/src/magnet.rs @@ -41,6 +41,17 @@ impl Magnet { } } +impl std::fmt::Display for Magnet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "magnet:?xt=urn:btih:{}&tr={}", + self.info_hash.as_string(), + self.trackers.join("&tr=") + ) + } +} + #[cfg(test)] mod tests { #[test] diff --git a/crates/rqbit/Cargo.toml b/crates/rqbit/Cargo.toml index a0c5410..e2a1d97 100644 --- a/crates/rqbit/Cargo.toml +++ b/crates/rqbit/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" [features] default = ["sha1-system", "default-tls", "webui"] -tokio-console = ["console-subscriber",] +tokio-console = ["console-subscriber", "tokio/tracing"] webui = ["librqbit/webui"] timed_existence = ["librqbit/timed_existence"] sha1-system = ["librqbit/sha1-system"] @@ -25,7 +25,7 @@ rust-tls = ["librqbit/rust-tls"] [dependencies] librqbit = {path="../librqbit", default-features=false, version = "3.3.0"} dht = {path="../dht", package="librqbit-dht", version="3.1.0"} -tokio = {version = "1", features = ["macros", "rt-multi-thread", "tracing"]} +tokio = {version = "1", features = ["macros", "rt-multi-thread"]} console-subscriber = {version = "0.2", optional = true} anyhow = "1" clap = {version = "4", features = ["derive", "deprecated"]} diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 4e79124..dfdab9a 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -279,6 +279,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> disable_dht: opts.disable_dht, disable_dht_persistence: opts.disable_dht_persistence, dht_config: None, + persistence: true, peer_id: None, peer_opts: Some(PeerConnectionOptions { connect_timeout: Some(opts.peer_connect_timeout), @@ -342,15 +343,13 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> match &opts.subcommand { SubCommand::Server(server_opts) => match &server_opts.subcommand { ServerSubcommand::Start(start_opts) => { - let session = Arc::new( - Session::new_with_opts( - PathBuf::from(&start_opts.output_folder), - spawner, - sopts, - ) - .await - .context("error initializing rqbit session")?, - ); + let session = Session::new_with_opts( + PathBuf::from(&start_opts.output_folder), + spawner, + sopts, + ) + .await + .context("error initializing rqbit session")?; spawn( "stats_printer", trace_span!("stats_printer"), @@ -416,21 +415,19 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> } Ok(()) } else { - let session = Arc::new( - Session::new_with_opts( - download_opts - .output_folder - .as_ref() - .map(PathBuf::from) - .context( - "output_folder is required if can't connect to an existing server", - )?, - spawner, - sopts, - ) - .await - .context("error initializing rqbit session")?, - ); + let session = Session::new_with_opts( + download_opts + .output_folder + .as_ref() + .map(PathBuf::from) + .context( + "output_folder is required if can't connect to an existing server", + )?, + spawner, + sopts, + ) + .await + .context("error initializing rqbit session")?; spawn( "stats_printer", trace_span!("stats_printer"), From 1bea1f92352657196ee646394b2e7dce69610bf6 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 10:11:40 +0000 Subject: [PATCH 27/38] Better API for stats printing --- crates/dht/examples/dht.rs | 2 +- crates/librqbit/examples/ubuntu.rs | 12 +- crates/librqbit/src/http_api.rs | 129 +----------- crates/librqbit/src/lib.rs | 1 - crates/librqbit/src/session.rs | 1 - .../src/torrent_state/live/stats/snapshot.rs | 2 - crates/librqbit/src/torrent_state/mod.rs | 65 +++++- crates/librqbit/src/torrent_state/stats.rs | 198 ++++++++++++++++++ 8 files changed, 273 insertions(+), 137 deletions(-) create mode 100644 crates/librqbit/src/torrent_state/stats.rs diff --git a/crates/dht/examples/dht.rs b/crates/dht/examples/dht.rs index cac7bd6..8862cdc 100644 --- a/crates/dht/examples/dht.rs +++ b/crates/dht/examples/dht.rs @@ -17,7 +17,7 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let dht = Dht::new().await.context("error initializing DHT")?; - let mut stream = dht.get_peers(info_hash).await?; + let mut stream = dht.get_peers(info_hash)?; let stats_printer = async { loop { diff --git a/crates/librqbit/examples/ubuntu.rs b/crates/librqbit/examples/ubuntu.rs index c4932b0..7c136de 100644 --- a/crates/librqbit/examples/ubuntu.rs +++ b/crates/librqbit/examples/ubuntu.rs @@ -16,6 +16,10 @@ const MAGNET_LINK: &str = "magnet:?xt=urn:btih:cab507494d02ebb1178b38f2e9d7be299 #[tokio::main] async fn main() -> Result<(), anyhow::Error> { // Output logs to console. + match std::env::var("RUST_LOG") { + Ok(_) => {} + Err(_) => std::env::set_var("RUST_LOG", "info"), + } tracing_subscriber::fmt::init(); let output_dir = std::env::args() @@ -44,19 +48,21 @@ async fn main() -> Result<(), anyhow::Error> { .await .context("error adding torrent")? { - AddTorrentResponse::Added(handle) => handle, + AddTorrentResponse::Added(_, handle) => handle, // For a brand new session other variants won't happen. _ => unreachable!(), }; + info!("Details: {:?}", &handle.info().info); + // Print stats periodically. tokio::spawn({ let handle = handle.clone(); async move { loop { tokio::time::sleep(Duration::from_secs(1)).await; - let stats = handle.torrent_state().stats_snapshot(); - info!("stats: {stats:?}"); + let stats = handle.stats(); + info!("{stats:}"); } } }); diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 4266c6a..c44e68d 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -11,9 +11,7 @@ use librqbit_core::id20::Id20; use librqbit_core::torrent_metainfo::TorrentMetaV1Info; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; -use std::sync::atomic::Ordering; use std::sync::Arc; -use std::time::{Duration, Instant}; use tokio::sync::mpsc::UnboundedSender; use tracing::{info, warn}; @@ -24,8 +22,8 @@ use crate::session::{ AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, TorrentId, }; use crate::torrent_state::peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot}; -use crate::torrent_state::stats::snapshot::StatsSnapshot; -use crate::torrent_state::{ManagedTorrentHandle, ManagedTorrentState, TorrentStateLive}; +use crate::torrent_state::stats::{LiveStats, TorrentStats}; +use crate::torrent_state::ManagedTorrentHandle; // Public API #[derive(Clone)] @@ -233,27 +231,6 @@ impl HttpApi { type Result = std::result::Result; -#[derive(Serialize, Default)] -struct Speed { - mbps: f64, - human_readable: String, -} - -impl Speed { - fn new(mbps: f64) -> Self { - Self { - mbps, - human_readable: format!("{mbps:.2} MiB/s"), - } - } -} - -impl From for Speed { - fn from(mbps: f64) -> Self { - Self::new(mbps) - } -} - #[derive(Serialize)] struct TorrentListResponseItem { id: usize, @@ -281,45 +258,6 @@ pub struct TorrentDetailsResponse { pub files: Vec, } -struct DurationWithHumanReadable(Duration); - -impl Serialize for DurationWithHumanReadable { - fn serialize(&self, serializer: S) -> core::result::Result - where - S: serde::Serializer, - { - #[derive(Serialize)] - struct Tmp { - duration: Duration, - human_readable: String, - } - Tmp { - duration: self.0, - human_readable: format!("{:?}", self.0), - } - .serialize(serializer) - } -} - -#[derive(Serialize, Default)] -struct LiveStats { - snapshot: StatsSnapshot, - average_piece_download_time: Option, - download_speed: Speed, - all_time_download_speed: Speed, - time_remaining: Option, -} - -#[derive(Serialize)] -struct StatsResponse { - state: &'static str, - error: Option, - progress_bytes: u64, - total_bytes: u64, - finished: bool, - live: Option, -} - #[derive(Serialize, Deserialize)] pub struct ApiAddTorrentResponse { pub id: Option, @@ -393,7 +331,6 @@ impl TorrentAddQueryParams { // Private HTTP API internals. Agnostic of web framework. struct ApiInternal { - startup_time: Instant, session: Arc, rust_log_reload_tx: Option>, } @@ -403,7 +340,6 @@ type ApiState = Arc; impl ApiInternal { pub fn new(session: Arc, rust_log_reload_tx: Option>) -> Self { Self { - startup_time: Instant::now(), session, rust_log_reload_tx, } @@ -543,70 +479,15 @@ impl ApiInternal { Ok(dht.with_routing_table(|r| r.clone())) } - fn make_live_stats(&self, live: &TorrentStateLive) -> LiveStats { - let snapshot = live.stats_snapshot(); - let estimator = live.speed_estimator(); - - // Poor mans download speed computation - let elapsed = self.startup_time.elapsed(); - let downloaded_bytes = snapshot.downloaded_and_checked_bytes; - let downloaded_mb = downloaded_bytes as f64 / 1024f64 / 1024f64; - - LiveStats { - average_piece_download_time: snapshot.average_piece_download_time(), - snapshot, - all_time_download_speed: (downloaded_mb / elapsed.as_secs_f64()).into(), - download_speed: estimator.download_mbps().into(), - time_remaining: estimator.time_remaining().map(DurationWithHumanReadable), - } - } - fn api_stats_v0(&self, idx: TorrentId) -> Result { let mgr = self.mgr_handle(idx)?; let live = mgr.live().context("torrent not live")?; - Ok(self.make_live_stats(&live)) + Ok(LiveStats::from(&*live)) } - fn api_stats_v1(&self, idx: TorrentId) -> Result { + fn api_stats_v1(&self, idx: TorrentId) -> Result { let mgr = self.mgr_handle(idx)?; - let mut resp = StatsResponse { - total_bytes: mgr.info().lengths.total_length(), - state: "", - error: None, - progress_bytes: 0, - finished: false, - live: None, - }; - - mgr.with_state(|s| { - match s { - ManagedTorrentState::Initializing(i) => { - resp.state = "initializing"; - resp.progress_bytes = i.checked_bytes.load(Ordering::Relaxed); - } - ManagedTorrentState::Paused(p) => { - resp.state = "paused"; - resp.progress_bytes = p.have_bytes; - resp.finished = p.have_bytes == resp.total_bytes; - } - ManagedTorrentState::Live(l) => { - resp.state = "live"; - let live_stats = self.make_live_stats(l); - resp.progress_bytes = live_stats.snapshot.have_bytes; - resp.finished = resp.progress_bytes == resp.total_bytes; - resp.live = Some(live_stats); - } - ManagedTorrentState::Error(e) => { - resp.state = "error"; - resp.error = Some(format!("{:?}", e)) - } - ManagedTorrentState::None => { - resp.state = "error"; - resp.error = Some("bug: torrent in broken \"None\" state".to_string()); - } - } - Ok(resp) - }) + Ok(mgr.stats()) } fn api_dump_haves(&self, idx: usize) -> Result { diff --git a/crates/librqbit/src/lib.rs b/crates/librqbit/src/lib.rs index 4be7c2d..ac5c18a 100644 --- a/crates/librqbit/src/lib.rs +++ b/crates/librqbit/src/lib.rs @@ -8,7 +8,6 @@ pub mod peer_connection; pub mod peer_info_reader; pub mod session; pub mod spawn_utils; -// pub mod torrent_manager; pub mod torrent_state; pub mod tracker_comms; pub mod type_aliases; diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index ddf82dc..0e3cc4e 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -1,7 +1,6 @@ use std::{ borrow::Cow, collections::{HashMap, HashSet}, - fs::{File, OpenOptions}, io::{BufReader, BufWriter, Read}, net::SocketAddr, path::PathBuf, diff --git a/crates/librqbit/src/torrent_state/live/stats/snapshot.rs b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs index 6331b6a..2e5dd53 100644 --- a/crates/librqbit/src/torrent_state/live/stats/snapshot.rs +++ b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs @@ -14,8 +14,6 @@ pub struct StatsSnapshot { pub initially_needed_bytes: u64, pub remaining_bytes: u64, pub total_bytes: u64, - // #[serde(skip)] - // pub time: Instant, pub total_piece_download_ms: u64, pub peer_stats: AggregatePeerStats, } diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index e95c5b3..2cc9fcd 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -1,12 +1,14 @@ pub mod initializing; pub mod live; pub mod paused; +pub mod stats; pub mod utils; use std::collections::HashSet; use std::net::SocketAddr; use std::path::Path; use std::path::PathBuf; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Weak; use std::time::Duration; @@ -31,10 +33,12 @@ use url::Url; use crate::chunk_tracker::ChunkTracker; use crate::spawn_utils::spawn; use crate::spawn_utils::BlockingSpawner; +use crate::torrent_state::stats::LiveStats; use initializing::TorrentStateInitializing; use self::paused::TorrentStatePaused; +use self::stats::TorrentStats; pub enum ManagedTorrentState { Initializing(Arc), @@ -251,12 +255,63 @@ impl ManagedTorrent { } } + pub fn stats(&self) -> TorrentStats { + let mut resp = TorrentStats { + total_bytes: self.info().lengths.total_length(), + state: "", + error: None, + progress_bytes: 0, + finished: false, + live: None, + }; + + self.with_state(|s| { + match s { + ManagedTorrentState::Initializing(i) => { + resp.state = "initializing"; + resp.progress_bytes = i.checked_bytes.load(Ordering::Relaxed); + } + ManagedTorrentState::Paused(p) => { + resp.state = "paused"; + resp.progress_bytes = p.have_bytes; + resp.finished = p.have_bytes == resp.total_bytes; + } + ManagedTorrentState::Live(l) => { + resp.state = "live"; + let live_stats = LiveStats::from(l.as_ref()); + resp.progress_bytes = live_stats.snapshot.have_bytes; + resp.finished = resp.progress_bytes == resp.total_bytes; + resp.live = Some(live_stats); + } + ManagedTorrentState::Error(e) => { + resp.state = "error"; + resp.error = Some(format!("{:?}", e)) + } + ManagedTorrentState::None => { + resp.state = "error"; + resp.error = Some("bug: torrent in broken \"None\" state".to_string()); + } + } + resp + }) + } + pub async fn wait_until_completed(&self) -> anyhow::Result<()> { - // TODO: rewrite - self.live() - .context("torrent isn't live")? - .wait_until_completed() - .await; + // TODO: rewrite, this polling is horrible + let live = loop { + let live = self.with_state(|s| match s { + ManagedTorrentState::Initializing(_) | ManagedTorrentState::Paused(_) => Ok(None), + ManagedTorrentState::Live(l) => Ok(Some(l.clone())), + ManagedTorrentState::Error(e) => bail!("{:?}", e), + ManagedTorrentState::None => bail!("bug: torrent state is None"), + })?; + if let Some(live) = live { + break live; + } + tokio::time::sleep(Duration::from_secs(1)).await; + }; + + live.wait_until_completed().await; Ok(()) } } diff --git a/crates/librqbit/src/torrent_state/stats.rs b/crates/librqbit/src/torrent_state/stats.rs new file mode 100644 index 0000000..8940aa7 --- /dev/null +++ b/crates/librqbit/src/torrent_state/stats.rs @@ -0,0 +1,198 @@ +use std::time::Duration; + +use serde::Serialize; + +use super::{live::stats::snapshot::StatsSnapshot, TorrentStateLive}; +use size_format::SizeFormatterBinary as SF; + +#[derive(Serialize, Default, Debug)] +pub struct LiveStats { + pub snapshot: StatsSnapshot, + pub average_piece_download_time: Option, + pub download_speed: Speed, + pub time_remaining: Option, +} + +impl std::fmt::Display for LiveStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "down speed: {}", self.download_speed)?; + if let Some(time_remaining) = &self.time_remaining { + write!(f, " eta: {time_remaining}")?; + } + Ok(()) + } +} + +impl From<&TorrentStateLive> for LiveStats { + fn from(live: &TorrentStateLive) -> Self { + let snapshot = live.stats_snapshot(); + let estimator = live.speed_estimator(); + + Self { + average_piece_download_time: snapshot.average_piece_download_time(), + snapshot, + download_speed: estimator.download_mbps().into(), + time_remaining: estimator.time_remaining().map(DurationWithHumanReadable), + } + } +} + +#[derive(Serialize, Debug)] +pub struct TorrentStats { + pub state: &'static str, + pub error: Option, + pub progress_bytes: u64, + pub total_bytes: u64, + pub finished: bool, + pub live: Option, +} + +impl std::fmt::Display for TorrentStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: ", self.state)?; + if let Some(error) = &self.error { + return write!(f, "{error}"); + } + write!( + f, + "{} ({})", + self.progress_percent_human_readable(), + self.progress_bytes_human_readable() + )?; + if let Some(live) = &self.live { + write!(f, " [{live}]")?; + } + Ok(()) + } +} + +impl TorrentStats { + pub fn progress_percent_human_readable(&self) -> impl std::fmt::Display { + struct Percents { + progress: u64, + total: u64, + } + impl std::fmt::Display for Percents { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.total == 0 { + return write!(f, "N/A"); + } + let pct = self.progress as f64 / self.total as f64 * 100f64; + write!(f, "{pct:.2}%") + } + } + Percents { + progress: self.progress_bytes, + total: self.total_bytes, + } + } + + pub fn progress_bytes_human_readable(&self) -> impl std::fmt::Display { + struct Progress { + progress: u64, + total: u64, + } + impl std::fmt::Display for Progress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} / {}", SF::new(self.progress), SF::new(self.total)) + } + } + Progress { + progress: self.progress_bytes, + total: self.total_bytes, + } + } +} + +fn format_seconds_to_time(seconds: u64, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let hours = seconds / 3600; + let minutes = (seconds % 3600) / 60; + let seconds = seconds % 60; + + if hours > 0 { + write!(f, "{}h {}m", hours, minutes) + } else if minutes > 0 { + write!(f, "{}m {}s", minutes, seconds) + } else { + write!(f, "{}s", seconds) + } +} + +pub struct DurationWithHumanReadable(Duration); + +impl core::fmt::Display for DurationWithHumanReadable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result { + format_seconds_to_time(self.0.as_secs(), f) + } +} + +impl core::fmt::Debug for DurationWithHumanReadable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + +impl Serialize for DurationWithHumanReadable { + fn serialize(&self, serializer: S) -> core::result::Result + where + S: serde::Serializer, + { + #[derive(Serialize)] + struct Tmp { + duration: Duration, + human_readable: String, + } + Tmp { + duration: self.0, + human_readable: format!("{}", self), + } + .serialize(serializer) + } +} + +#[derive(Default)] +pub struct Speed { + pub mbps: f64, +} + +impl Speed { + fn new(mbps: f64) -> Self { + Self { mbps } + } +} + +impl From for Speed { + fn from(mbps: f64) -> Self { + Self::new(mbps) + } +} + +impl core::fmt::Display for Speed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:.2} MiB/s", self.mbps) + } +} + +impl core::fmt::Debug for Speed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + +impl Serialize for Speed { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + #[derive(Serialize)] + struct Tmp { + mbps: f64, + human_readable: String, + } + Tmp { + mbps: self.mbps, + human_readable: format!("{:?}", self.mbps), + } + .serialize(serializer) + } +} From 1c53aeba2f2ce1255ce887e59fa68aff25a2abcd Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 11:21:45 +0000 Subject: [PATCH 28/38] Fix a couple bugs --- crates/librqbit/examples/ubuntu.rs | 8 ++----- crates/librqbit/src/http_api.rs | 26 +++++++++++++------- crates/librqbit/src/session.rs | 6 ++++- crates/librqbit/src/torrent_state/stats.rs | 2 +- crates/librqbit/webui/src/index.tsx | 17 +++++++------ crates/rqbit/src/main.rs | 28 +++++++++++++++++----- 6 files changed, 57 insertions(+), 30 deletions(-) diff --git a/crates/librqbit/examples/ubuntu.rs b/crates/librqbit/examples/ubuntu.rs index 7c136de..f93d27c 100644 --- a/crates/librqbit/examples/ubuntu.rs +++ b/crates/librqbit/examples/ubuntu.rs @@ -36,12 +36,8 @@ async fn main() -> Result<(), anyhow::Error> { .add_torrent( AddTorrent::from_url(MAGNET_LINK), Some(AddTorrentOptions { - // Set this to true to allow writing on top of existing files. - // If the file is partially downloaded, librqbit will only download the - // missing pieces. - // - // Otherwise it will throw an error that the file exists. - overwrite: false, + // Allow writing on top of existing files. + overwrite: true, ..Default::default() }), ) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index c44e68d..0eb8f5b 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -38,7 +38,11 @@ impl HttpApi { } } - pub async fn make_http_api_and_run(self, addr: SocketAddr) -> anyhow::Result<()> { + pub async fn make_http_api_and_run( + self, + addr: SocketAddr, + read_only: bool, + ) -> anyhow::Result<()> { let state = self.inner; async fn api_root() -> impl IntoResponse { @@ -160,22 +164,26 @@ impl HttpApi { state.api_set_rust_log(new_value).map(axum::Json) } - #[allow(unused_mut)] let mut app = Router::new() .route("/", get(api_root)) + .route("/rust_log", post(set_rust_log)) .route("/dht/stats", get(dht_stats)) .route("/dht/table", get(dht_table)) - .route("/torrents", get(torrents_list).post(torrents_post)) + .route("/torrents", get(torrents_list)) .route("/torrents/:id", get(torrent_details)) .route("/torrents/:id/haves", get(torrent_haves)) .route("/torrents/:id/stats", get(torrent_stats_v0)) .route("/torrents/:id/stats/v1", get(torrent_stats_v1)) - .route("/torrents/:id/peer_stats", get(peer_stats)) - .route("/torrents/:id/pause", post(torrent_action_pause)) - .route("/torrents/:id/start", post(torrent_action_start)) - .route("/torrents/:id/forget", post(torrent_action_forget)) - .route("/torrents/:id/delete", post(torrent_action_delete)) - .route("/rust_log", post(set_rust_log)); + .route("/torrents/:id/peer_stats", get(peer_stats)); + + if !read_only { + app = app + .route("/torrents", post(torrents_post)) + .route("/torrents/:id/pause", post(torrent_action_pause)) + .route("/torrents/:id/start", post(torrent_action_start)) + .route("/torrents/:id/forget", post(torrent_action_forget)) + .route("/torrents/:id/delete", post(torrent_action_delete)); + } #[cfg(feature = "webui")] { diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 0e3cc4e..b8531a9 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -210,6 +210,8 @@ pub struct SessionOptions { pub disable_dht: bool, pub disable_dht_persistence: bool, pub persistence: bool, + // Will default to output_folder/.rqbit-session.json + pub persistence_filename: Option, pub dht_config: Option, pub peer_id: Option, pub peer_opts: Option, @@ -240,7 +242,9 @@ impl Session { Some(dht) }; let peer_opts = opts.peer_opts.unwrap_or_default(); - let session_filename = output_folder.join(".rqbit-session.json"); + let session_filename = opts + .persistence_filename + .unwrap_or_else(|| output_folder.join(".rqbit-session.json")); let session = Arc::new(Self { persistence_filename: session_filename, peer_id, diff --git a/crates/librqbit/src/torrent_state/stats.rs b/crates/librqbit/src/torrent_state/stats.rs index 8940aa7..2d68d8d 100644 --- a/crates/librqbit/src/torrent_state/stats.rs +++ b/crates/librqbit/src/torrent_state/stats.rs @@ -191,7 +191,7 @@ impl Serialize for Speed { } Tmp { mbps: self.mbps, - human_readable: format!("{:?}", self.mbps), + human_readable: format!("{}", self), } .serialize(serializer) } diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 4eed9db..bdcff52 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -1,7 +1,7 @@ import { MouseEventHandler, StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner, Table } from 'react-bootstrap'; -import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED } from './api'; +import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR } from './api'; interface Error { text: string, @@ -171,13 +171,16 @@ const TorrentRow: React.FC<{ return `${peer_stats.live} / ${peer_stats.seen}`; } - const formatDownloadSped = () => { + const formatDownloadSpeed = () => { if (finished) { return 'Completed'; } - if (state == STATE_INITIALIZING) { - return 'Checking files'; + switch (state) { + case STATE_PAUSED: return 'Paused'; + case STATE_INITIALIZING: return 'Checking files'; + case STATE_ERROR: return 'Error'; } + return statsResponse.live?.download_speed.human_readable ?? "N/A"; } @@ -206,14 +209,14 @@ const TorrentRow: React.FC<{ {statsResponse ? <> {`${formatBytes(totalBytes)} `} - + - {formatDownloadSped()} + {formatDownloadSpeed()} {getCompletionETA(statsResponse)} {formatPeersString()} @@ -393,7 +396,7 @@ const UploadButton = ({ buttonText, onClick, data, resetData, variant }) => { const response = await API.uploadTorrent(data, { listOnly: true }); setFileList(response.details.files); } catch (e) { - setFileListError({ text: 'Error listing torrent', details: e }); + setFileListError({ text: 'Error uploading torrent', details: e }); } finally { setLoading(false); } diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index dfdab9a..bf96634 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -77,6 +77,13 @@ struct Opts { struct ServerStartOptions { /// The output folder to write to. If not exists, it will be created. output_folder: String, + #[arg( + long = "disable-persistence", + help = "Disable server persistence. It will not read or write its state to disk." + )] + disable_persistence: bool, + #[arg(long = "persistence-filename")] + persistence_filename: Option, } #[derive(Parser)] @@ -275,11 +282,12 @@ fn main() -> anyhow::Result<()> { async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> { let logging_reload_tx = init_logging(&opts); - let sopts = SessionOptions { + let mut sopts = SessionOptions { disable_dht: opts.disable_dht, disable_dht_persistence: opts.disable_dht_persistence, dht_config: None, - persistence: true, + persistence: false, + persistence_filename: None, peer_id: None, peer_opts: Some(PeerConnectionOptions { connect_timeout: Some(opts.peer_connect_timeout), @@ -343,6 +351,9 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> match &opts.subcommand { SubCommand::Server(server_opts) => match &server_opts.subcommand { ServerSubcommand::Start(start_opts) => { + sopts.persistence = !start_opts.disable_persistence; + sopts.persistence_filename = + start_opts.persistence_filename.clone().map(PathBuf::from); let session = Session::new_with_opts( PathBuf::from(&start_opts.output_folder), spawner, @@ -358,7 +369,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> let http_api = HttpApi::new(session, Some(logging_reload_tx)); let http_api_listen_addr = opts.http_api_listen_addr; http_api - .make_http_api_and_run(http_api_listen_addr) + .make_http_api_and_run(http_api_listen_addr, false) .await .context("error starting HTTP API") } @@ -438,7 +449,9 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> spawn( "http_api", error_span!("http_api"), - http_api.clone().make_http_api_and_run(http_api_listen_addr), + http_api + .clone() + .make_http_api_and_run(http_api_listen_addr, true), ); let mut added = false; @@ -504,8 +517,11 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> if download_opts.exit_on_finish { let results = futures::future::join_all( handles.iter().map(|h| h.wait_until_completed()), - ); - results.await; + ) + .await; + if results.iter().any(|r| r.is_err()) { + anyhow::bail!("some downloads failed") + } info!("All downloads completed, exiting"); Ok(()) } else { From 051a231482c5292bd094b169724c49a4e5dec510 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 11:27:30 +0000 Subject: [PATCH 29/38] Fixing UI --- crates/librqbit/webui/dist/app.js | 20 ++++++++++---------- crates/librqbit/webui/src/index.tsx | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/librqbit/webui/dist/app.js b/crates/librqbit/webui/dist/app.js index 6d7fa13..765ee33 100644 --- a/crates/librqbit/webui/dist/app.js +++ b/crates/librqbit/webui/dist/app.js @@ -1,4 +1,4 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function Gl(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var wa={exports:{}},Yl={},Sa={exports:{}},F={};/** +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function Yl(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Sa={exports:{}},Xl={},ka={exports:{}},F={};/** * @license React * react.production.min.js * @@ -6,7 +6,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var $r=Symbol.for("react.element"),Hd=Symbol.for("react.portal"),Wd=Symbol.for("react.fragment"),Vd=Symbol.for("react.strict_mode"),Qd=Symbol.for("react.profiler"),Kd=Symbol.for("react.provider"),Gd=Symbol.for("react.context"),Yd=Symbol.for("react.forward_ref"),Xd=Symbol.for("react.suspense"),Zd=Symbol.for("react.memo"),Jd=Symbol.for("react.lazy"),Ju=Symbol.iterator;function qd(e){return e===null||typeof e!="object"?null:(e=Ju&&e[Ju]||e["@@iterator"],typeof e=="function"?e:null)}var ka={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Ea=Object.assign,xa={};function Un(e,t,n){this.props=e,this.context=t,this.refs=xa,this.updater=n||ka}Un.prototype.isReactComponent={};Un.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Un.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Ca(){}Ca.prototype=Un.prototype;function Yi(e,t,n){this.props=e,this.context=t,this.refs=xa,this.updater=n||ka}var Xi=Yi.prototype=new Ca;Xi.constructor=Yi;Ea(Xi,Un.prototype);Xi.isPureReactComponent=!0;var qu=Array.isArray,Na=Object.prototype.hasOwnProperty,Zi={current:null},Ta={key:!0,ref:!0,__self:!0,__source:!0};function _a(e,t,n){var r,l={},o=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(o=""+t.key),t)Na.call(t,r)&&!Ta.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,A=x[D];if(0>>1;Dl(qe,O))Rel(mt,qe)?(x[D]=mt,x[Re]=O,D=Re):(x[D]=qe,x[je]=O,D=je);else if(Rel(mt,O))x[D]=mt,x[Re]=O,D=Re;else break e}}return L}function l(x,L){var O=x.sortIndex-L.sortIndex;return O!==0?O:x.id-L.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],a=[],d=1,m=null,p=3,g=!1,w=!1,k=!1,R=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function h(x){for(var L=n(a);L!==null;){if(L.callback===null)r(a);else if(L.startTime<=x)r(a),L.sortIndex=L.expirationTime,t(s,L);else break;L=n(a)}}function S(x){if(k=!1,h(x),!w)if(n(s)!==null)w=!0,_e(C);else{var L=n(a);L!==null&&Ke(S,L.startTime-x)}}function C(x,L){w=!1,k&&(k=!1,f(j),j=-1),g=!0;var O=p;try{for(h(L),m=n(s);m!==null&&(!(m.expirationTime>L)||x&&!ie());){var D=m.callback;if(typeof D=="function"){m.callback=null,p=m.priorityLevel;var A=D(m.expirationTime<=L);L=e.unstable_now(),typeof A=="function"?m.callback=A:m===n(s)&&r(s),h(L)}else r(s);m=n(s)}if(m!==null)var fe=!0;else{var je=n(a);je!==null&&Ke(S,je.startTime-L),fe=!1}return fe}finally{m=null,p=O,g=!1}}var N=!1,T=null,j=-1,U=5,P=-1;function ie(){return!(e.unstable_now()-Px||125D?(x.sortIndex=O,t(a,x),n(s)===null&&x===n(a)&&(k?(f(j),j=-1):k=!0,Ke(S,O-D))):(x.sortIndex=A,t(s,x),w||g||(w=!0,_e(C))),x},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(x){var L=p;return function(){var O=p;p=L;try{return x.apply(this,arguments)}finally{p=O}}}})(Oa);La.exports=Oa;var ap=La.exports;/** + */(function(e){function t(x,L){var O=x.length;x.push(L);e:for(;0>>1,A=x[D];if(0>>1;Dl(qe,O))Rel(mt,qe)?(x[D]=mt,x[Re]=O,D=Re):(x[D]=qe,x[je]=O,D=je);else if(Rel(mt,O))x[D]=mt,x[Re]=O,D=Re;else break e}}return L}function l(x,L){var O=x.sortIndex-L.sortIndex;return O!==0?O:x.id-L.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],a=[],d=1,m=null,p=3,g=!1,w=!1,k=!1,R=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function h(x){for(var L=n(a);L!==null;){if(L.callback===null)r(a);else if(L.startTime<=x)r(a),L.sortIndex=L.expirationTime,t(s,L);else break;L=n(a)}}function S(x){if(k=!1,h(x),!w)if(n(s)!==null)w=!0,_e(C);else{var L=n(a);L!==null&&Ke(S,L.startTime-x)}}function C(x,L){w=!1,k&&(k=!1,f(j),j=-1),g=!0;var O=p;try{for(h(L),m=n(s);m!==null&&(!(m.expirationTime>L)||x&&!ie());){var D=m.callback;if(typeof D=="function"){m.callback=null,p=m.priorityLevel;var A=D(m.expirationTime<=L);L=e.unstable_now(),typeof A=="function"?m.callback=A:m===n(s)&&r(s),h(L)}else r(s);m=n(s)}if(m!==null)var fe=!0;else{var je=n(a);je!==null&&Ke(S,je.startTime-L),fe=!1}return fe}finally{m=null,p=O,g=!1}}var N=!1,T=null,j=-1,B=5,P=-1;function ie(){return!(e.unstable_now()-Px||125D?(x.sortIndex=O,t(a,x),n(s)===null&&x===n(a)&&(k?(f(j),j=-1):k=!0,Ke(S,O-D))):(x.sortIndex=A,t(s,x),w||g||(w=!0,_e(C))),x},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(x){var L=p;return function(){var O=p;p=L;try{return x.apply(this,arguments)}finally{p=O}}}})(Pa);Oa.exports=Pa;var cp=Oa.exports;/** * @license React * react-dom.production.min.js * @@ -30,15 +30,15 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Pa=y,Ce=ap;function E(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Yo=Object.prototype.hasOwnProperty,cp=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,es={},ts={};function fp(e){return Yo.call(ts,e)?!0:Yo.call(es,e)?!1:cp.test(e)?ts[e]=!0:(es[e]=!0,!1)}function dp(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function pp(e,t,n,r){if(t===null||typeof t>"u"||dp(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function he(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var oe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){oe[e]=new he(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];oe[t]=new he(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){oe[e]=new he(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){oe[e]=new he(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){oe[e]=new he(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){oe[e]=new he(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){oe[e]=new he(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){oe[e]=new he(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){oe[e]=new he(e,5,!1,e.toLowerCase(),null,!1,!1)});var qi=/[\-:]([a-z])/g;function bi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!1,!1)});oe.xlinkHref=new he("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!0,!0)});function eu(e,t,n,r){var l=oe.hasOwnProperty(t)?oe[t]:null;(l!==null?l.type!==0:r||!(2"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Xo=Object.prototype.hasOwnProperty,fp=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,es={},ts={};function dp(e){return Xo.call(ts,e)?!0:Xo.call(es,e)?!1:fp.test(e)?ts[e]=!0:(es[e]=!0,!1)}function pp(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function mp(e,t,n,r){if(t===null||typeof t>"u"||pp(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function he(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var oe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){oe[e]=new he(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];oe[t]=new he(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){oe[e]=new he(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){oe[e]=new he(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){oe[e]=new he(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){oe[e]=new he(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){oe[e]=new he(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){oe[e]=new he(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){oe[e]=new he(e,5,!1,e.toLowerCase(),null,!1,!1)});var qi=/[\-:]([a-z])/g;function bi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!1,!1)});oe.xlinkHref=new he("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!0,!0)});function eu(e,t,n,r){var l=oe.hasOwnProperty(t)?oe[t]:null;(l!==null?l.type!==0:r||!(2u||l[i]!==o[u]){var s=` -`+l[i].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{yo=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?rr(e):""}function mp(e){switch(e.tag){case 5:return rr(e.type);case 16:return rr("Lazy");case 13:return rr("Suspense");case 19:return rr("SuspenseList");case 0:case 2:case 15:return e=go(e.type,!1),e;case 11:return e=go(e.type.render,!1),e;case 1:return e=go(e.type,!0),e;default:return""}}function qo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case pn:return"Fragment";case dn:return"Portal";case Xo:return"Profiler";case tu:return"StrictMode";case Zo:return"Suspense";case Jo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case za:return(e.displayName||"Context")+".Consumer";case Ma:return(e._context.displayName||"Context")+".Provider";case nu:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ru:return t=e.displayName||null,t!==null?t:qo(e.type)||"Memo";case gt:t=e._payload,e=e._init;try{return qo(e(t))}catch{}}return null}function hp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return qo(t);case 8:return t===tu?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Ft(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Da(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function vp(e){var t=Da(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Qr(e){e._valueTracker||(e._valueTracker=vp(e))}function Ia(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Da(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Sl(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function bo(e,t){var n=t.checked;return X({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function rs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Ft(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Aa(e,t){t=t.checked,t!=null&&eu(e,"checked",t,!1)}function ei(e,t){Aa(e,t);var n=Ft(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ti(e,t.type,n):t.hasOwnProperty("defaultValue")&&ti(e,t.type,Ft(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ls(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ti(e,t,n){(t!=="number"||Sl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var lr=Array.isArray;function Nn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Kr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function gr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var sr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},yp=["Webkit","ms","Moz","O"];Object.keys(sr).forEach(function(e){yp.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),sr[t]=sr[e]})});function Wa(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||sr.hasOwnProperty(e)&&sr[e]?(""+t).trim():t+"px"}function Va(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Wa(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var gp=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function li(e,t){if(t){if(gp[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(E(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(E(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(E(61))}if(t.style!=null&&typeof t.style!="object")throw Error(E(62))}}function oi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ii=null;function lu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ui=null,Tn=null,_n=null;function us(e){if(e=Ar(e)){if(typeof ui!="function")throw Error(E(280));var t=e.stateNode;t&&(t=bl(t),ui(e.stateNode,e.type,t))}}function Qa(e){Tn?_n?_n.push(e):_n=[e]:Tn=e}function Ka(){if(Tn){var e=Tn,t=_n;if(_n=Tn=null,us(e),t)for(e=0;e>>=0,e===0?32:31-(Rp(e)/Lp|0)|0}var Gr=64,Yr=4194304;function or(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Cl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=or(u):(o&=i,o!==0&&(r=or(o)))}else i=n&~l,i!==0?r=or(i):o!==0&&(r=or(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Dr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Be(t),e[t]=n}function Mp(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=cr),vs=String.fromCharCode(32),ys=!1;function dc(e,t){switch(e){case"keyup":return sm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function pc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var mn=!1;function cm(e,t){switch(e){case"compositionend":return pc(t);case"keypress":return t.which!==32?null:(ys=!0,vs);case"textInput":return e=t.data,e===vs&&ys?null:e;default:return null}}function fm(e,t){if(mn)return e==="compositionend"||!du&&dc(e,t)?(e=cc(),fl=au=xt=null,mn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ks(n)}}function yc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?yc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function gc(){for(var e=window,t=Sl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Sl(e.document)}return t}function pu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Sm(e){var t=gc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&yc(n.ownerDocument.documentElement,n)){if(r!==null&&pu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Es(n,o);var i=Es(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,hn=null,pi=null,dr=null,mi=!1;function xs(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;mi||hn==null||hn!==Sl(r)||(r=hn,"selectionStart"in r&&pu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),dr&&Cr(dr,r)||(dr=r,r=_l(pi,"onSelect"),0gn||(e.current=Si[gn],Si[gn]=null,gn--)}function B(e,t){gn++,Si[gn]=e.current,e.current=t}var Mt={},ce=$t(Mt),ge=$t(!1),Zt=Mt;function Fn(e,t){var n=e.type.contextTypes;if(!n)return Mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function we(e){return e=e.childContextTypes,e!=null}function Rl(){V(ge),V(ce)}function Ls(e,t,n){if(ce.current!==Mt)throw Error(E(168));B(ce,t),B(ge,n)}function _c(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(E(108,hp(e)||"Unknown",l));return X({},n,r)}function Ll(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Mt,Zt=ce.current,B(ce,e),B(ge,ge.current),!0}function Os(e,t,n){var r=e.stateNode;if(!r)throw Error(E(169));n?(e=_c(e,t,Zt),r.__reactInternalMemoizedMergedChildContext=e,V(ge),V(ce),B(ce,e)):V(ge),B(ge,n)}var et=null,eo=!1,Po=!1;function jc(e){et===null?et=[e]:et.push(e)}function Pm(e){eo=!0,jc(e)}function Dt(){if(!Po&&et!==null){Po=!0;var e=0,t=I;try{var n=et;for(I=1;e>=i,l-=i,nt=1<<32-Be(t)+l|n<j?(U=T,T=null):U=T.sibling;var P=p(f,T,h[j],S);if(P===null){T===null&&(T=U);break}e&&T&&P.alternate===null&&t(f,T),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P,T=U}if(j===h.length)return n(f,T),Q&&At(f,j),C;if(T===null){for(;jj?(U=T,T=null):U=T.sibling;var ie=p(f,T,P.value,S);if(ie===null){T===null&&(T=U);break}e&&T&&ie.alternate===null&&t(f,T),c=o(ie,c,j),N===null?C=ie:N.sibling=ie,N=ie,T=U}if(P.done)return n(f,T),Q&&At(f,j),C;if(T===null){for(;!P.done;j++,P=h.next())P=m(f,P.value,S),P!==null&&(c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return Q&&At(f,j),C}for(T=r(f,T);!P.done;j++,P=h.next())P=g(T,f,j,P.value,S),P!==null&&(e&&P.alternate!==null&&T.delete(P.key===null?j:P.key),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return e&&T.forEach(function(Ve){return t(f,Ve)}),Q&&At(f,j),C}function R(f,c,h,S){if(typeof h=="object"&&h!==null&&h.type===pn&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Vr:e:{for(var C=h.key,N=c;N!==null;){if(N.key===C){if(C=h.type,C===pn){if(N.tag===7){n(f,N.sibling),c=l(N,h.props.children),c.return=f,f=c;break e}}else if(N.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===gt&&Is(C)===N.type){n(f,N.sibling),c=l(N,h.props),c.ref=er(f,N,h),c.return=f,f=c;break e}n(f,N);break}else t(f,N);N=N.sibling}h.type===pn?(c=Yt(h.props.children,f.mode,S,h.key),c.return=f,f=c):(S=wl(h.type,h.key,h.props,null,f.mode,S),S.ref=er(f,c,h),S.return=f,f=S)}return i(f);case dn:e:{for(N=h.key;c!==null;){if(c.key===N)if(c.tag===4&&c.stateNode.containerInfo===h.containerInfo&&c.stateNode.implementation===h.implementation){n(f,c.sibling),c=l(c,h.children||[]),c.return=f,f=c;break e}else{n(f,c);break}else t(f,c);c=c.sibling}c=Uo(h,f.mode,S),c.return=f,f=c}return i(f);case gt:return N=h._init,R(f,c,N(h._payload),S)}if(lr(h))return w(f,c,h,S);if(Xn(h))return k(f,c,h,S);tl(f,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,c!==null&&c.tag===6?(n(f,c.sibling),c=l(c,h),c.return=f,f=c):(n(f,c),c=Ao(h,f.mode,S),c.return=f,f=c),i(f)):n(f,c)}return R}var zn=$c(!0),Dc=$c(!1),Ur={},Je=$t(Ur),jr=$t(Ur),Rr=$t(Ur);function Kt(e){if(e===Ur)throw Error(E(174));return e}function Eu(e,t){switch(B(Rr,t),B(jr,e),B(Je,Ur),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:ri(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=ri(t,e)}V(Je),B(Je,t)}function $n(){V(Je),V(jr),V(Rr)}function Ic(e){Kt(Rr.current);var t=Kt(Je.current),n=ri(t,e.type);t!==n&&(B(jr,e),B(Je,n))}function xu(e){jr.current===e&&(V(Je),V(jr))}var G=$t(0);function $l(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var Fo=[];function Cu(){for(var e=0;en?n:4,e(!0);var r=Mo.transition;Mo.transition={};try{e(!1),t()}finally{I=n,Mo.transition=r}}function ef(){return $e().memoizedState}function $m(e,t,n){var r=Ot(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},tf(e))nf(t,n);else if(n=Pc(e,t,n,r),n!==null){var l=pe();He(n,e,r,l),rf(n,t,r)}}function Dm(e,t,n){var r=Ot(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(tf(e))nf(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,We(u,i)){var s=t.interleaved;s===null?(l.next=l,Su(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Pc(e,t,l,r),n!==null&&(l=pe(),He(n,e,r,l),rf(n,t,r))}}function tf(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function nf(e,t){pr=Dl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function rf(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,iu(e,n)}}var Il={readContext:ze,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},Im={readContext:ze,useCallback:function(e,t){return Ye().memoizedState=[e,t===void 0?null:t],e},useContext:ze,useEffect:Us,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,hl(4194308,4,Xc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return hl(4194308,4,e,t)},useInsertionEffect:function(e,t){return hl(4,2,e,t)},useMemo:function(e,t){var n=Ye();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ye();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=$m.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ye();return e={current:e},t.memoizedState=e},useState:As,useDebugValue:Ru,useDeferredValue:function(e){return Ye().memoizedState=e},useTransition:function(){var e=As(!1),t=e[0];return e=zm.bind(null,e[1]),Ye().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,l=Ye();if(Q){if(n===void 0)throw Error(E(407));n=n()}else{if(n=t(),ne===null)throw Error(E(349));qt&30||Bc(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Us(Wc.bind(null,r,o,e),[e]),r.flags|=2048,Pr(9,Hc.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ye(),t=ne.identifierPrefix;if(Q){var n=rt,r=nt;n=(r&~(1<<32-Be(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Lr++,0")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{go=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?rr(e):""}function hp(e){switch(e.tag){case 5:return rr(e.type);case 16:return rr("Lazy");case 13:return rr("Suspense");case 19:return rr("SuspenseList");case 0:case 2:case 15:return e=wo(e.type,!1),e;case 11:return e=wo(e.type.render,!1),e;case 1:return e=wo(e.type,!0),e;default:return""}}function bo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case pn:return"Fragment";case dn:return"Portal";case Zo:return"Profiler";case tu:return"StrictMode";case Jo:return"Suspense";case qo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case $a:return(e.displayName||"Context")+".Consumer";case za:return(e._context.displayName||"Context")+".Provider";case nu:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ru:return t=e.displayName||null,t!==null?t:bo(e.type)||"Memo";case gt:t=e._payload,e=e._init;try{return bo(e(t))}catch{}}return null}function vp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return bo(t);case 8:return t===tu?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Ft(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ia(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function yp(e){var t=Ia(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Qr(e){e._valueTracker||(e._valueTracker=yp(e))}function Aa(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Ia(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function kl(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ei(e,t){var n=t.checked;return X({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function rs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Ft(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Ba(e,t){t=t.checked,t!=null&&eu(e,"checked",t,!1)}function ti(e,t){Ba(e,t);var n=Ft(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ni(e,t.type,n):t.hasOwnProperty("defaultValue")&&ni(e,t.type,Ft(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ls(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ni(e,t,n){(t!=="number"||kl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var lr=Array.isArray;function Nn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Kr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function gr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var sr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},gp=["Webkit","ms","Moz","O"];Object.keys(sr).forEach(function(e){gp.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),sr[t]=sr[e]})});function Va(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||sr.hasOwnProperty(e)&&sr[e]?(""+t).trim():t+"px"}function Qa(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Va(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var wp=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function oi(e,t){if(t){if(wp[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(E(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(E(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(E(61))}if(t.style!=null&&typeof t.style!="object")throw Error(E(62))}}function ii(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ui=null;function lu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var si=null,Tn=null,_n=null;function us(e){if(e=Ar(e)){if(typeof si!="function")throw Error(E(280));var t=e.stateNode;t&&(t=eo(t),si(e.stateNode,e.type,t))}}function Ka(e){Tn?_n?_n.push(e):_n=[e]:Tn=e}function Ga(){if(Tn){var e=Tn,t=_n;if(_n=Tn=null,us(e),t)for(e=0;e>>=0,e===0?32:31-(Lp(e)/Op|0)|0}var Gr=64,Yr=4194304;function or(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Nl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=or(u):(o&=i,o!==0&&(r=or(o)))}else i=n&~l,i!==0?r=or(i):o!==0&&(r=or(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Dr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ue(t),e[t]=n}function zp(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=cr),vs=String.fromCharCode(32),ys=!1;function pc(e,t){switch(e){case"keyup":return am.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function mc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var mn=!1;function fm(e,t){switch(e){case"compositionend":return mc(t);case"keypress":return t.which!==32?null:(ys=!0,vs);case"textInput":return e=t.data,e===vs&&ys?null:e;default:return null}}function dm(e,t){if(mn)return e==="compositionend"||!du&&pc(e,t)?(e=fc(),fl=au=xt=null,mn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ks(n)}}function gc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?gc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function wc(){for(var e=window,t=kl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=kl(e.document)}return t}function pu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function km(e){var t=wc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&gc(n.ownerDocument.documentElement,n)){if(r!==null&&pu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Es(n,o);var i=Es(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,hn=null,mi=null,dr=null,hi=!1;function xs(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;hi||hn==null||hn!==kl(r)||(r=hn,"selectionStart"in r&&pu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),dr&&Cr(dr,r)||(dr=r,r=jl(mi,"onSelect"),0gn||(e.current=ki[gn],ki[gn]=null,gn--)}function U(e,t){gn++,ki[gn]=e.current,e.current=t}var Mt={},ce=$t(Mt),ge=$t(!1),Zt=Mt;function Fn(e,t){var n=e.type.contextTypes;if(!n)return Mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function we(e){return e=e.childContextTypes,e!=null}function Ll(){V(ge),V(ce)}function Ls(e,t,n){if(ce.current!==Mt)throw Error(E(168));U(ce,t),U(ge,n)}function jc(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(E(108,vp(e)||"Unknown",l));return X({},n,r)}function Ol(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Mt,Zt=ce.current,U(ce,e),U(ge,ge.current),!0}function Os(e,t,n){var r=e.stateNode;if(!r)throw Error(E(169));n?(e=jc(e,t,Zt),r.__reactInternalMemoizedMergedChildContext=e,V(ge),V(ce),U(ce,e)):V(ge),U(ge,n)}var et=null,to=!1,Fo=!1;function Rc(e){et===null?et=[e]:et.push(e)}function Fm(e){to=!0,Rc(e)}function Dt(){if(!Fo&&et!==null){Fo=!0;var e=0,t=I;try{var n=et;for(I=1;e>=i,l-=i,nt=1<<32-Ue(t)+l|n<j?(B=T,T=null):B=T.sibling;var P=p(f,T,h[j],S);if(P===null){T===null&&(T=B);break}e&&T&&P.alternate===null&&t(f,T),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P,T=B}if(j===h.length)return n(f,T),Q&&At(f,j),C;if(T===null){for(;jj?(B=T,T=null):B=T.sibling;var ie=p(f,T,P.value,S);if(ie===null){T===null&&(T=B);break}e&&T&&ie.alternate===null&&t(f,T),c=o(ie,c,j),N===null?C=ie:N.sibling=ie,N=ie,T=B}if(P.done)return n(f,T),Q&&At(f,j),C;if(T===null){for(;!P.done;j++,P=h.next())P=m(f,P.value,S),P!==null&&(c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return Q&&At(f,j),C}for(T=r(f,T);!P.done;j++,P=h.next())P=g(T,f,j,P.value,S),P!==null&&(e&&P.alternate!==null&&T.delete(P.key===null?j:P.key),c=o(P,c,j),N===null?C=P:N.sibling=P,N=P);return e&&T.forEach(function(Ve){return t(f,Ve)}),Q&&At(f,j),C}function R(f,c,h,S){if(typeof h=="object"&&h!==null&&h.type===pn&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Vr:e:{for(var C=h.key,N=c;N!==null;){if(N.key===C){if(C=h.type,C===pn){if(N.tag===7){n(f,N.sibling),c=l(N,h.props.children),c.return=f,f=c;break e}}else if(N.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===gt&&Is(C)===N.type){n(f,N.sibling),c=l(N,h.props),c.ref=er(f,N,h),c.return=f,f=c;break e}n(f,N);break}else t(f,N);N=N.sibling}h.type===pn?(c=Yt(h.props.children,f.mode,S,h.key),c.return=f,f=c):(S=wl(h.type,h.key,h.props,null,f.mode,S),S.ref=er(f,c,h),S.return=f,f=S)}return i(f);case dn:e:{for(N=h.key;c!==null;){if(c.key===N)if(c.tag===4&&c.stateNode.containerInfo===h.containerInfo&&c.stateNode.implementation===h.implementation){n(f,c.sibling),c=l(c,h.children||[]),c.return=f,f=c;break e}else{n(f,c);break}else t(f,c);c=c.sibling}c=Uo(h,f.mode,S),c.return=f,f=c}return i(f);case gt:return N=h._init,R(f,c,N(h._payload),S)}if(lr(h))return w(f,c,h,S);if(Xn(h))return k(f,c,h,S);tl(f,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,c!==null&&c.tag===6?(n(f,c.sibling),c=l(c,h),c.return=f,f=c):(n(f,c),c=Bo(h,f.mode,S),c.return=f,f=c),i(f)):n(f,c)}return R}var zn=Dc(!0),Ic=Dc(!1),Br={},Je=$t(Br),jr=$t(Br),Rr=$t(Br);function Kt(e){if(e===Br)throw Error(E(174));return e}function Eu(e,t){switch(U(Rr,t),U(jr,e),U(Je,Br),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:li(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=li(t,e)}V(Je),U(Je,t)}function $n(){V(Je),V(jr),V(Rr)}function Ac(e){Kt(Rr.current);var t=Kt(Je.current),n=li(t,e.type);t!==n&&(U(jr,e),U(Je,n))}function xu(e){jr.current===e&&(V(Je),V(jr))}var G=$t(0);function Dl(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var Mo=[];function Cu(){for(var e=0;en?n:4,e(!0);var r=zo.transition;zo.transition={};try{e(!1),t()}finally{I=n,zo.transition=r}}function tf(){return $e().memoizedState}function Dm(e,t,n){var r=Ot(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},nf(e))rf(t,n);else if(n=Fc(e,t,n,r),n!==null){var l=pe();He(n,e,r,l),lf(n,t,r)}}function Im(e,t,n){var r=Ot(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(nf(e))rf(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,We(u,i)){var s=t.interleaved;s===null?(l.next=l,Su(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Fc(e,t,l,r),n!==null&&(l=pe(),He(n,e,r,l),lf(n,t,r))}}function nf(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function rf(e,t){pr=Il=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function lf(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,iu(e,n)}}var Al={readContext:ze,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},Am={readContext:ze,useCallback:function(e,t){return Ye().memoizedState=[e,t===void 0?null:t],e},useContext:ze,useEffect:Bs,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,hl(4194308,4,Zc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return hl(4194308,4,e,t)},useInsertionEffect:function(e,t){return hl(4,2,e,t)},useMemo:function(e,t){var n=Ye();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ye();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Dm.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ye();return e={current:e},t.memoizedState=e},useState:As,useDebugValue:Ru,useDeferredValue:function(e){return Ye().memoizedState=e},useTransition:function(){var e=As(!1),t=e[0];return e=$m.bind(null,e[1]),Ye().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,l=Ye();if(Q){if(n===void 0)throw Error(E(407));n=n()}else{if(n=t(),ne===null)throw Error(E(349));qt&30||Hc(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Bs(Vc.bind(null,r,o,e),[e]),r.flags|=2048,Pr(9,Wc.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ye(),t=ne.identifierPrefix;if(Q){var n=rt,r=nt;n=(r&~(1<<32-Ue(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Lr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Xe]=t,e[_r]=r,pf(e,t,!1,!1),t.stateNode=e;e:{switch(i=oi(n,r),n){case"dialog":W("cancel",e),W("close",e),l=r;break;case"iframe":case"object":case"embed":W("load",e),l=r;break;case"video":case"audio":for(l=0;lIn&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304)}else{if(!r)if(e=$l(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),tr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!Q)return se(t),null}else 2*J()-o.renderingStartTime>In&&n!==1073741824&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=J(),t.sibling=null,n=G.current,B(G,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return zu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ke&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(E(156,t.tag))}function Km(e,t){switch(hu(t),t.tag){case 1:return we(t.type)&&Rl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return $n(),V(ge),V(ce),Cu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return xu(t),null;case 13:if(V(G),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(E(340));Mn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(G),null;case 4:return $n(),null;case 10:return wu(t.type._context),null;case 22:case 23:return zu(),null;case 24:return null;default:return null}}var rl=!1,ae=!1,Gm=typeof WeakSet=="function"?WeakSet:Set,_=null;function En(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Z(e,t,r)}else n.current=null}function Pi(e,t,n){try{n()}catch(r){Z(e,t,r)}}var Xs=!1;function Ym(e,t){if(hi=Nl,e=gc(),pu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,a=0,d=0,m=e,p=null;t:for(;;){for(var g;m!==n||l!==0&&m.nodeType!==3||(u=i+l),m!==o||r!==0&&m.nodeType!==3||(s=i+r),m.nodeType===3&&(i+=m.nodeValue.length),(g=m.firstChild)!==null;)p=m,m=g;for(;;){if(m===e)break t;if(p===n&&++a===l&&(u=i),p===o&&++d===r&&(s=i),(g=m.nextSibling)!==null)break;m=p,p=m.parentNode}m=g}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(vi={focusedElem:e,selectionRange:n},Nl=!1,_=t;_!==null;)if(t=_,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_=e;else for(;_!==null;){t=_;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var k=w.memoizedProps,R=w.memoizedState,f=t.stateNode,c=f.getSnapshotBeforeUpdate(t.elementType===t.type?k:Ie(t.type,k),R);f.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(E(163))}}catch(S){Z(t,t.return,S)}if(e=t.sibling,e!==null){e.return=t.return,_=e;break}_=t.return}return w=Xs,Xs=!1,w}function mr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Pi(t,n,o)}l=l.next}while(l!==r)}}function ro(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Fi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function vf(e){var t=e.alternate;t!==null&&(e.alternate=null,vf(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Xe],delete t[_r],delete t[wi],delete t[Lm],delete t[Om])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function yf(e){return e.tag===5||e.tag===3||e.tag===4}function Zs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||yf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Mi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=jl));else if(r!==4&&(e=e.child,e!==null))for(Mi(e,t,n),e=e.sibling;e!==null;)Mi(e,t,n),e=e.sibling}function zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(zi(e,t,n),e=e.sibling;e!==null;)zi(e,t,n),e=e.sibling}var re=null,Ae=!1;function ht(e,t,n){for(n=n.child;n!==null;)gf(e,t,n),n=n.sibling}function gf(e,t,n){if(Ze&&typeof Ze.onCommitFiberUnmount=="function")try{Ze.onCommitFiberUnmount(Xl,n)}catch{}switch(n.tag){case 5:ae||En(n,t);case 6:var r=re,l=Ae;re=null,ht(e,t,n),re=r,Ae=l,re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?Oo(e.parentNode,n):e.nodeType===1&&Oo(e,n),Er(e)):Oo(re,n.stateNode));break;case 4:r=re,l=Ae,re=n.stateNode.containerInfo,Ae=!0,ht(e,t,n),re=r,Ae=l;break;case 0:case 11:case 14:case 15:if(!ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Pi(n,t,i),l=l.next}while(l!==r)}ht(e,t,n);break;case 1:if(!ae&&(En(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Z(n,t,u)}ht(e,t,n);break;case 21:ht(e,t,n);break;case 22:n.mode&1?(ae=(r=ae)||n.memoizedState!==null,ht(e,t,n),ae=r):ht(e,t,n);break;default:ht(e,t,n)}}function Js(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Gm),t.forEach(function(r){var l=rh.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function De(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Zm(r/1960))-r,10e?16:e,Ct===null)var r=!1;else{if(e=Ct,Ct=null,Bl=0,z&6)throw Error(E(331));var l=z;for(z|=4,_=e.current;_!==null;){var o=_,i=o.child;if(_.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sJ()-Fu?Gt(e,0):Pu|=n),Se(e,t)}function Tf(e,t){t===0&&(e.mode&1?(t=Yr,Yr<<=1,!(Yr&130023424)&&(Yr=4194304)):t=1);var n=pe();e=st(e,t),e!==null&&(Dr(e,t,n),Se(e,n))}function nh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Tf(e,n)}function rh(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(E(314))}r!==null&&r.delete(t),Tf(e,n)}var _f;_f=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ge.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,Vm(e,t,n);ye=!!(e.flags&131072)}else ye=!1,Q&&t.flags&1048576&&Rc(t,Pl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;vl(e,t),e=t.pendingProps;var l=Fn(t,ce.current);Rn(t,n),l=Tu(null,t,r,e,l,n);var o=_u();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,we(r)?(o=!0,Ll(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ku(t),l.updater=to,t.stateNode=l,l._reactInternals=t,Ni(t,r,e,n),t=ji(null,t,r,!0,o,n)):(t.tag=0,Q&&o&&mu(t),de(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(vl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=oh(r),e=Ie(r,e),l){case 0:t=_i(null,t,r,e,n);break e;case 1:t=Ks(null,t,r,e,n);break e;case 11:t=Vs(null,t,r,e,n);break e;case 14:t=Qs(null,t,r,Ie(r.type,e),n);break e}throw Error(E(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),_i(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Ks(e,t,r,l,n);case 3:e:{if(cf(t),e===null)throw Error(E(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Fc(e,t),zl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=Dn(Error(E(423)),t),t=Gs(e,t,r,n,l);break e}else if(r!==l){l=Dn(Error(E(424)),t),t=Gs(e,t,r,n,l);break e}else for(Ee=jt(t.stateNode.containerInfo.firstChild),xe=t,Q=!0,Ue=null,n=Dc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Mn(),r===l){t=at(e,t,n);break e}de(e,t,r,n)}t=t.child}return t;case 5:return Ic(t),e===null&&Ei(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,yi(r,l)?i=null:o!==null&&yi(r,o)&&(t.flags|=32),af(e,t),de(e,t,i,n),t.child;case 6:return e===null&&Ei(t),null;case 13:return ff(e,t,n);case 4:return Eu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=zn(t,null,r,n):de(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Vs(e,t,r,l,n);case 7:return de(e,t,t.pendingProps,n),t.child;case 8:return de(e,t,t.pendingProps.children,n),t.child;case 12:return de(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,B(Fl,r._currentValue),r._currentValue=i,o!==null)if(We(o.value,i)){if(o.children===l.children&&!ge.current){t=at(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=lt(-1,n&-n),s.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var d=a.pending;d===null?s.next=s:(s.next=d.next,d.next=s),a.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),xi(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(E(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),xi(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}de(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Rn(t,n),l=ze(l),r=r(l),t.flags|=1,de(e,t,r,n),t.child;case 14:return r=t.type,l=Ie(r,t.pendingProps),l=Ie(r.type,l),Qs(e,t,r,l,n);case 15:return uf(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),vl(e,t),t.tag=1,we(r)?(e=!0,Ll(t)):e=!1,Rn(t,n),zc(t,r,l),Ni(t,r,l,n),ji(null,t,r,!0,e,n);case 19:return df(e,t,n);case 22:return sf(e,t,n)}throw Error(E(156,t.tag))};function jf(e,t){return ba(e,t)}function lh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Fe(e,t,n,r){return new lh(e,t,n,r)}function Du(e){return e=e.prototype,!(!e||!e.isReactComponent)}function oh(e){if(typeof e=="function")return Du(e)?1:0;if(e!=null){if(e=e.$$typeof,e===nu)return 11;if(e===ru)return 14}return 2}function Pt(e,t){var n=e.alternate;return n===null?(n=Fe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function wl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Du(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case pn:return Yt(n.children,l,o,t);case tu:i=8,l|=8;break;case Xo:return e=Fe(12,n,t,l|2),e.elementType=Xo,e.lanes=o,e;case Zo:return e=Fe(13,n,t,l),e.elementType=Zo,e.lanes=o,e;case Jo:return e=Fe(19,n,t,l),e.elementType=Jo,e.lanes=o,e;case $a:return oo(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ma:i=10;break e;case za:i=9;break e;case nu:i=11;break e;case ru:i=14;break e;case gt:i=16,r=null;break e}throw Error(E(130,e==null?e:typeof e,""))}return t=Fe(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Yt(e,t,n,r){return e=Fe(7,e,r,t),e.lanes=n,e}function oo(e,t,n,r){return e=Fe(22,e,r,t),e.elementType=$a,e.lanes=n,e.stateNode={isHidden:!1},e}function Ao(e,t,n){return e=Fe(6,e,null,t),e.lanes=n,e}function Uo(e,t,n){return t=Fe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function ih(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=So(0),this.expirationTimes=So(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=So(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Iu(e,t,n,r,l,o,i,u,s){return e=new ih(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Fe(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ku(o),e}function uh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Pf)}catch(e){console.error(e)}}Pf(),Ra.exports=Ne;var Ff=Ra.exports;const Cn=Gl(Ff);var oa=Ff;Go.createRoot=oa.createRoot,Go.hydrateRoot=oa.hydrateRoot;var Mf={exports:{}};/*! +`+o.stack}return{value:e,source:t,stack:l,digest:null}}function Io(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function _i(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var Hm=typeof WeakMap=="function"?WeakMap:Map;function of(e,t,n){n=lt(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){Ul||(Ul=!0,Di=r),_i(e,t)},n}function uf(e,t,n){n=lt(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){_i(e,t)}}var o=e.stateNode;return o!==null&&typeof o.componentDidCatch=="function"&&(n.callback=function(){_i(e,t),typeof r!="function"&&(Lt===null?Lt=new Set([this]):Lt.add(this));var i=t.stack;this.componentDidCatch(t.value,{componentStack:i!==null?i:""})}),n}function Us(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Hm;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=nh.bind(null,e,t,n),t.then(e,e))}function Hs(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Ws(e,t,n,r,l){return e.mode&1?(e.flags|=65536,e.lanes=l,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=lt(-1,1),t.tag=2,Rt(n,t,1))),n.lanes|=1),e)}var Wm=dt.ReactCurrentOwner,ye=!1;function de(e,t,n,r){t.child=e===null?Ic(t,null,n,r):zn(t,e.child,n,r)}function Vs(e,t,n,r,l){n=n.render;var o=t.ref;return Rn(t,l),r=Tu(e,t,n,r,o,l),n=_u(),e!==null&&!ye?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,at(e,t,l)):(Q&&n&&mu(t),t.flags|=1,de(e,t,r,l),t.child)}function Qs(e,t,n,r,l){if(e===null){var o=n.type;return typeof o=="function"&&!Du(o)&&o.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=o,sf(e,t,o,r,l)):(e=wl(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(o=e.child,!(e.lanes&l)){var i=o.memoizedProps;if(n=n.compare,n=n!==null?n:Cr,n(i,r)&&e.ref===t.ref)return at(e,t,l)}return t.flags|=1,e=Pt(o,r),e.ref=t.ref,e.return=t,t.child=e}function sf(e,t,n,r,l){if(e!==null){var o=e.memoizedProps;if(Cr(o,r)&&e.ref===t.ref)if(ye=!1,t.pendingProps=r=o,(e.lanes&l)!==0)e.flags&131072&&(ye=!0);else return t.lanes=e.lanes,at(e,t,l)}return ji(e,t,n,r,l)}function af(e,t,n){var r=t.pendingProps,l=r.children,o=e!==null?e.memoizedState:null;if(r.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},U(xn,ke),ke|=n;else{if(!(n&1073741824))return e=o!==null?o.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,U(xn,ke),ke|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=o!==null?o.baseLanes:n,U(xn,ke),ke|=r}else o!==null?(r=o.baseLanes|n,t.memoizedState=null):r=n,U(xn,ke),ke|=r;return de(e,t,l,n),t.child}function cf(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function ji(e,t,n,r,l){var o=we(n)?Zt:ce.current;return o=Fn(t,o),Rn(t,l),n=Tu(e,t,n,r,o,l),r=_u(),e!==null&&!ye?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,at(e,t,l)):(Q&&r&&mu(t),t.flags|=1,de(e,t,n,l),t.child)}function Ks(e,t,n,r,l){if(we(n)){var o=!0;Ol(t)}else o=!1;if(Rn(t,l),t.stateNode===null)vl(e,t),$c(t,n,r),Ti(t,n,r,l),r=!0;else if(e===null){var i=t.stateNode,u=t.memoizedProps;i.props=u;var s=i.context,a=n.contextType;typeof a=="object"&&a!==null?a=ze(a):(a=we(n)?Zt:ce.current,a=Fn(t,a));var d=n.getDerivedStateFromProps,m=typeof d=="function"||typeof i.getSnapshotBeforeUpdate=="function";m||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(u!==r||s!==a)&&Ds(t,i,r,a),wt=!1;var p=t.memoizedState;i.state=p,$l(t,r,i,l),s=t.memoizedState,u!==r||p!==s||ge.current||wt?(typeof d=="function"&&(Ni(t,n,d,r),s=t.memoizedState),(u=wt||$s(t,n,u,r,p,s,a))?(m||typeof i.UNSAFE_componentWillMount!="function"&&typeof i.componentWillMount!="function"||(typeof i.componentWillMount=="function"&&i.componentWillMount(),typeof i.UNSAFE_componentWillMount=="function"&&i.UNSAFE_componentWillMount()),typeof i.componentDidMount=="function"&&(t.flags|=4194308)):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=s),i.props=r,i.state=s,i.context=a,r=u):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{i=t.stateNode,Mc(e,t),u=t.memoizedProps,a=t.type===t.elementType?u:Ie(t.type,u),i.props=a,m=t.pendingProps,p=i.context,s=n.contextType,typeof s=="object"&&s!==null?s=ze(s):(s=we(n)?Zt:ce.current,s=Fn(t,s));var g=n.getDerivedStateFromProps;(d=typeof g=="function"||typeof i.getSnapshotBeforeUpdate=="function")||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(u!==m||p!==s)&&Ds(t,i,r,s),wt=!1,p=t.memoizedState,i.state=p,$l(t,r,i,l);var w=t.memoizedState;u!==m||p!==w||ge.current||wt?(typeof g=="function"&&(Ni(t,n,g,r),w=t.memoizedState),(a=wt||$s(t,n,a,r,p,w,s)||!1)?(d||typeof i.UNSAFE_componentWillUpdate!="function"&&typeof i.componentWillUpdate!="function"||(typeof i.componentWillUpdate=="function"&&i.componentWillUpdate(r,w,s),typeof i.UNSAFE_componentWillUpdate=="function"&&i.UNSAFE_componentWillUpdate(r,w,s)),typeof i.componentDidUpdate=="function"&&(t.flags|=4),typeof i.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof i.componentDidUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=w),i.props=r,i.state=w,i.context=s,r=a):(typeof i.componentDidUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),r=!1)}return Ri(e,t,n,r,o,l)}function Ri(e,t,n,r,l,o){cf(e,t);var i=(t.flags&128)!==0;if(!r&&!i)return l&&Os(t,n,!1),at(e,t,o);r=t.stateNode,Wm.current=t;var u=i&&typeof n.getDerivedStateFromError!="function"?null:r.render();return t.flags|=1,e!==null&&i?(t.child=zn(t,e.child,null,o),t.child=zn(t,null,u,o)):de(e,t,u,o),t.memoizedState=r.state,l&&Os(t,n,!0),t.child}function ff(e){var t=e.stateNode;t.pendingContext?Ls(e,t.pendingContext,t.pendingContext!==t.context):t.context&&Ls(e,t.context,!1),Eu(e,t.containerInfo)}function Gs(e,t,n,r,l){return Mn(),vu(l),t.flags|=256,de(e,t,n,r),t.child}var Li={dehydrated:null,treeContext:null,retryLane:0};function Oi(e){return{baseLanes:e,cachePool:null,transitions:null}}function df(e,t,n){var r=t.pendingProps,l=G.current,o=!1,i=(t.flags&128)!==0,u;if((u=i)||(u=e!==null&&e.memoizedState===null?!1:(l&2)!==0),u?(o=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(l|=1),U(G,l&1),e===null)return xi(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?(t.mode&1?e.data==="$!"?t.lanes=8:t.lanes=1073741824:t.lanes=1,null):(i=r.children,e=r.fallback,o?(r=t.mode,o=t.child,i={mode:"hidden",children:i},!(r&1)&&o!==null?(o.childLanes=0,o.pendingProps=i):o=io(i,r,0,null),e=Yt(e,r,n,null),o.return=t,e.return=t,o.sibling=e,t.child=o,t.child.memoizedState=Oi(n),t.memoizedState=Li,e):Lu(t,i));if(l=e.memoizedState,l!==null&&(u=l.dehydrated,u!==null))return Vm(e,t,i,r,u,l,n);if(o){o=r.fallback,i=t.mode,l=e.child,u=l.sibling;var s={mode:"hidden",children:r.children};return!(i&1)&&t.child!==l?(r=t.child,r.childLanes=0,r.pendingProps=s,t.deletions=null):(r=Pt(l,s),r.subtreeFlags=l.subtreeFlags&14680064),u!==null?o=Pt(u,o):(o=Yt(o,i,n,null),o.flags|=2),o.return=t,r.return=t,r.sibling=o,t.child=r,r=o,o=t.child,i=e.child.memoizedState,i=i===null?Oi(n):{baseLanes:i.baseLanes|n,cachePool:null,transitions:i.transitions},o.memoizedState=i,o.childLanes=e.childLanes&~n,t.memoizedState=Li,r}return o=e.child,e=o.sibling,r=Pt(o,{mode:"visible",children:r.children}),!(t.mode&1)&&(r.lanes=n),r.return=t,r.sibling=null,e!==null&&(n=t.deletions,n===null?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=r,t.memoizedState=null,r}function Lu(e,t){return t=io({mode:"visible",children:t},e.mode,0,null),t.return=e,e.child=t}function nl(e,t,n,r){return r!==null&&vu(r),zn(t,e.child,null,n),e=Lu(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function Vm(e,t,n,r,l,o,i){if(n)return t.flags&256?(t.flags&=-257,r=Io(Error(E(422))),nl(e,t,i,r)):t.memoizedState!==null?(t.child=e.child,t.flags|=128,null):(o=r.fallback,l=t.mode,r=io({mode:"visible",children:r.children},l,0,null),o=Yt(o,l,i,null),o.flags|=2,r.return=t,o.return=t,r.sibling=o,t.child=r,t.mode&1&&zn(t,e.child,null,i),t.child.memoizedState=Oi(i),t.memoizedState=Li,o);if(!(t.mode&1))return nl(e,t,i,null);if(l.data==="$!"){if(r=l.nextSibling&&l.nextSibling.dataset,r)var u=r.dgst;return r=u,o=Error(E(419)),r=Io(o,r,void 0),nl(e,t,i,r)}if(u=(i&e.childLanes)!==0,ye||u){if(r=ne,r!==null){switch(i&-i){case 4:l=2;break;case 16:l=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:l=32;break;case 536870912:l=268435456;break;default:l=0}l=l&(r.suspendedLanes|i)?0:l,l!==0&&l!==o.retryLane&&(o.retryLane=l,st(e,l),He(r,e,l,-1))}return $u(),r=Io(Error(E(421))),nl(e,t,i,r)}return l.data==="$?"?(t.flags|=128,t.child=e.child,t=rh.bind(null,e),l._reactRetry=t,null):(e=o.treeContext,Ee=jt(l.nextSibling),xe=t,Q=!0,Be=null,e!==null&&(Le[Oe++]=nt,Le[Oe++]=rt,Le[Oe++]=Jt,nt=e.id,rt=e.overflow,Jt=t),t=Lu(t,r.children),t.flags|=4096,t)}function Ys(e,t,n){e.lanes|=t;var r=e.alternate;r!==null&&(r.lanes|=t),Ci(e.return,t,n)}function Ao(e,t,n,r,l){var o=e.memoizedState;o===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:l}:(o.isBackwards=t,o.rendering=null,o.renderingStartTime=0,o.last=r,o.tail=n,o.tailMode=l)}function pf(e,t,n){var r=t.pendingProps,l=r.revealOrder,o=r.tail;if(de(e,t,r.children,n),r=G.current,r&2)r=r&1|2,t.flags|=128;else{if(e!==null&&e.flags&128)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Ys(e,n,t);else if(e.tag===19)Ys(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if(U(G,r),!(t.mode&1))t.memoizedState=null;else switch(l){case"forwards":for(n=t.child,l=null;n!==null;)e=n.alternate,e!==null&&Dl(e)===null&&(l=n),n=n.sibling;n=l,n===null?(l=t.child,t.child=null):(l=n.sibling,n.sibling=null),Ao(t,!1,l,n,o);break;case"backwards":for(n=null,l=t.child,t.child=null;l!==null;){if(e=l.alternate,e!==null&&Dl(e)===null){t.child=l;break}e=l.sibling,l.sibling=n,n=l,l=e}Ao(t,!0,n,null,o);break;case"together":Ao(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function vl(e,t){!(t.mode&1)&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}function at(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),bt|=t.lanes,!(n&t.childLanes))return null;if(e!==null&&t.child!==e.child)throw Error(E(153));if(t.child!==null){for(e=t.child,n=Pt(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=Pt(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function Qm(e,t,n){switch(t.tag){case 3:ff(t),Mn();break;case 5:Ac(t);break;case 1:we(t.type)&&Ol(t);break;case 4:Eu(t,t.stateNode.containerInfo);break;case 10:var r=t.type._context,l=t.memoizedProps.value;U(Ml,r._currentValue),r._currentValue=l;break;case 13:if(r=t.memoizedState,r!==null)return r.dehydrated!==null?(U(G,G.current&1),t.flags|=128,null):n&t.child.childLanes?df(e,t,n):(U(G,G.current&1),e=at(e,t,n),e!==null?e.sibling:null);U(G,G.current&1);break;case 19:if(r=(n&t.childLanes)!==0,e.flags&128){if(r)return pf(e,t,n);t.flags|=128}if(l=t.memoizedState,l!==null&&(l.rendering=null,l.tail=null,l.lastEffect=null),U(G,G.current),r)break;return null;case 22:case 23:return t.lanes=0,af(e,t,n)}return at(e,t,n)}var mf,Pi,hf,vf;mf=function(e,t){for(var n=t.child;n!==null;){if(n.tag===5||n.tag===6)e.appendChild(n.stateNode);else if(n.tag!==4&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}};Pi=function(){};hf=function(e,t,n,r){var l=e.memoizedProps;if(l!==r){e=t.stateNode,Kt(Je.current);var o=null;switch(n){case"input":l=ei(e,l),r=ei(e,r),o=[];break;case"select":l=X({},l,{value:void 0}),r=X({},r,{value:void 0}),o=[];break;case"textarea":l=ri(e,l),r=ri(e,r),o=[];break;default:typeof l.onClick!="function"&&typeof r.onClick=="function"&&(e.onclick=Rl)}oi(n,r);var i;n=null;for(a in l)if(!r.hasOwnProperty(a)&&l.hasOwnProperty(a)&&l[a]!=null)if(a==="style"){var u=l[a];for(i in u)u.hasOwnProperty(i)&&(n||(n={}),n[i]="")}else a!=="dangerouslySetInnerHTML"&&a!=="children"&&a!=="suppressContentEditableWarning"&&a!=="suppressHydrationWarning"&&a!=="autoFocus"&&(yr.hasOwnProperty(a)?o||(o=[]):(o=o||[]).push(a,null));for(a in r){var s=r[a];if(u=l!=null?l[a]:void 0,r.hasOwnProperty(a)&&s!==u&&(s!=null||u!=null))if(a==="style")if(u){for(i in u)!u.hasOwnProperty(i)||s&&s.hasOwnProperty(i)||(n||(n={}),n[i]="");for(i in s)s.hasOwnProperty(i)&&u[i]!==s[i]&&(n||(n={}),n[i]=s[i])}else n||(o||(o=[]),o.push(a,n)),n=s;else a==="dangerouslySetInnerHTML"?(s=s?s.__html:void 0,u=u?u.__html:void 0,s!=null&&u!==s&&(o=o||[]).push(a,s)):a==="children"?typeof s!="string"&&typeof s!="number"||(o=o||[]).push(a,""+s):a!=="suppressContentEditableWarning"&&a!=="suppressHydrationWarning"&&(yr.hasOwnProperty(a)?(s!=null&&a==="onScroll"&&W("scroll",e),o||u===s||(o=[])):(o=o||[]).push(a,s))}n&&(o=o||[]).push("style",n);var a=o;(t.updateQueue=a)&&(t.flags|=4)}};vf=function(e,t,n,r){n!==r&&(t.flags|=4)};function tr(e,t){if(!Q)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;n!==null;)n.alternate!==null&&(r=n),n=n.sibling;r===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:r.sibling=null}}function se(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,r=0;if(t)for(var l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags&14680064,r|=l.flags&14680064,l.return=e,l=l.sibling;else for(l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags,r|=l.flags,l.return=e,l=l.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function Km(e,t,n){var r=t.pendingProps;switch(hu(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return se(t),null;case 1:return we(t.type)&&Ll(),se(t),null;case 3:return r=t.stateNode,$n(),V(ge),V(ce),Cu(),r.pendingContext&&(r.context=r.pendingContext,r.pendingContext=null),(e===null||e.child===null)&&(el(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&!(t.flags&256)||(t.flags|=1024,Be!==null&&(Bi(Be),Be=null))),Pi(e,t),se(t),null;case 5:xu(t);var l=Kt(Rr.current);if(n=t.type,e!==null&&t.stateNode!=null)hf(e,t,n,r,l),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!r){if(t.stateNode===null)throw Error(E(166));return se(t),null}if(e=Kt(Je.current),el(t)){r=t.stateNode,n=t.type;var o=t.memoizedProps;switch(r[Xe]=t,r[_r]=o,e=(t.mode&1)!==0,n){case"dialog":W("cancel",r),W("close",r);break;case"iframe":case"object":case"embed":W("load",r);break;case"video":case"audio":for(l=0;l<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Xe]=t,e[_r]=r,mf(e,t,!1,!1),t.stateNode=e;e:{switch(i=ii(n,r),n){case"dialog":W("cancel",e),W("close",e),l=r;break;case"iframe":case"object":case"embed":W("load",e),l=r;break;case"video":case"audio":for(l=0;lIn&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304)}else{if(!r)if(e=Dl(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),tr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!Q)return se(t),null}else 2*J()-o.renderingStartTime>In&&n!==1073741824&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=J(),t.sibling=null,n=G.current,U(G,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return zu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ke&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(E(156,t.tag))}function Gm(e,t){switch(hu(t),t.tag){case 1:return we(t.type)&&Ll(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return $n(),V(ge),V(ce),Cu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return xu(t),null;case 13:if(V(G),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(E(340));Mn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(G),null;case 4:return $n(),null;case 10:return wu(t.type._context),null;case 22:case 23:return zu(),null;case 24:return null;default:return null}}var rl=!1,ae=!1,Ym=typeof WeakSet=="function"?WeakSet:Set,_=null;function En(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Z(e,t,r)}else n.current=null}function Fi(e,t,n){try{n()}catch(r){Z(e,t,r)}}var Xs=!1;function Xm(e,t){if(vi=Tl,e=wc(),pu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,a=0,d=0,m=e,p=null;t:for(;;){for(var g;m!==n||l!==0&&m.nodeType!==3||(u=i+l),m!==o||r!==0&&m.nodeType!==3||(s=i+r),m.nodeType===3&&(i+=m.nodeValue.length),(g=m.firstChild)!==null;)p=m,m=g;for(;;){if(m===e)break t;if(p===n&&++a===l&&(u=i),p===o&&++d===r&&(s=i),(g=m.nextSibling)!==null)break;m=p,p=m.parentNode}m=g}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(yi={focusedElem:e,selectionRange:n},Tl=!1,_=t;_!==null;)if(t=_,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_=e;else for(;_!==null;){t=_;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var k=w.memoizedProps,R=w.memoizedState,f=t.stateNode,c=f.getSnapshotBeforeUpdate(t.elementType===t.type?k:Ie(t.type,k),R);f.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(E(163))}}catch(S){Z(t,t.return,S)}if(e=t.sibling,e!==null){e.return=t.return,_=e;break}_=t.return}return w=Xs,Xs=!1,w}function mr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Fi(t,n,o)}l=l.next}while(l!==r)}}function lo(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Mi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function yf(e){var t=e.alternate;t!==null&&(e.alternate=null,yf(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Xe],delete t[_r],delete t[Si],delete t[Om],delete t[Pm])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function gf(e){return e.tag===5||e.tag===3||e.tag===4}function Zs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||gf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Rl));else if(r!==4&&(e=e.child,e!==null))for(zi(e,t,n),e=e.sibling;e!==null;)zi(e,t,n),e=e.sibling}function $i(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for($i(e,t,n),e=e.sibling;e!==null;)$i(e,t,n),e=e.sibling}var re=null,Ae=!1;function ht(e,t,n){for(n=n.child;n!==null;)wf(e,t,n),n=n.sibling}function wf(e,t,n){if(Ze&&typeof Ze.onCommitFiberUnmount=="function")try{Ze.onCommitFiberUnmount(Zl,n)}catch{}switch(n.tag){case 5:ae||En(n,t);case 6:var r=re,l=Ae;re=null,ht(e,t,n),re=r,Ae=l,re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Ae?(e=re,n=n.stateNode,e.nodeType===8?Po(e.parentNode,n):e.nodeType===1&&Po(e,n),Er(e)):Po(re,n.stateNode));break;case 4:r=re,l=Ae,re=n.stateNode.containerInfo,Ae=!0,ht(e,t,n),re=r,Ae=l;break;case 0:case 11:case 14:case 15:if(!ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Fi(n,t,i),l=l.next}while(l!==r)}ht(e,t,n);break;case 1:if(!ae&&(En(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Z(n,t,u)}ht(e,t,n);break;case 21:ht(e,t,n);break;case 22:n.mode&1?(ae=(r=ae)||n.memoizedState!==null,ht(e,t,n),ae=r):ht(e,t,n);break;default:ht(e,t,n)}}function Js(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Ym),t.forEach(function(r){var l=lh.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function De(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Jm(r/1960))-r,10e?16:e,Ct===null)var r=!1;else{if(e=Ct,Ct=null,Hl=0,z&6)throw Error(E(331));var l=z;for(z|=4,_=e.current;_!==null;){var o=_,i=o.child;if(_.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sJ()-Fu?Gt(e,0):Pu|=n),Se(e,t)}function _f(e,t){t===0&&(e.mode&1?(t=Yr,Yr<<=1,!(Yr&130023424)&&(Yr=4194304)):t=1);var n=pe();e=st(e,t),e!==null&&(Dr(e,t,n),Se(e,n))}function rh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),_f(e,n)}function lh(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(E(314))}r!==null&&r.delete(t),_f(e,n)}var jf;jf=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ge.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,Qm(e,t,n);ye=!!(e.flags&131072)}else ye=!1,Q&&t.flags&1048576&&Lc(t,Fl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;vl(e,t),e=t.pendingProps;var l=Fn(t,ce.current);Rn(t,n),l=Tu(null,t,r,e,l,n);var o=_u();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,we(r)?(o=!0,Ol(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ku(t),l.updater=no,t.stateNode=l,l._reactInternals=t,Ti(t,r,e,n),t=Ri(null,t,r,!0,o,n)):(t.tag=0,Q&&o&&mu(t),de(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(vl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=ih(r),e=Ie(r,e),l){case 0:t=ji(null,t,r,e,n);break e;case 1:t=Ks(null,t,r,e,n);break e;case 11:t=Vs(null,t,r,e,n);break e;case 14:t=Qs(null,t,r,Ie(r.type,e),n);break e}throw Error(E(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),ji(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Ks(e,t,r,l,n);case 3:e:{if(ff(t),e===null)throw Error(E(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Mc(e,t),$l(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=Dn(Error(E(423)),t),t=Gs(e,t,r,n,l);break e}else if(r!==l){l=Dn(Error(E(424)),t),t=Gs(e,t,r,n,l);break e}else for(Ee=jt(t.stateNode.containerInfo.firstChild),xe=t,Q=!0,Be=null,n=Ic(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Mn(),r===l){t=at(e,t,n);break e}de(e,t,r,n)}t=t.child}return t;case 5:return Ac(t),e===null&&xi(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,gi(r,l)?i=null:o!==null&&gi(r,o)&&(t.flags|=32),cf(e,t),de(e,t,i,n),t.child;case 6:return e===null&&xi(t),null;case 13:return df(e,t,n);case 4:return Eu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=zn(t,null,r,n):de(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),Vs(e,t,r,l,n);case 7:return de(e,t,t.pendingProps,n),t.child;case 8:return de(e,t,t.pendingProps.children,n),t.child;case 12:return de(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,U(Ml,r._currentValue),r._currentValue=i,o!==null)if(We(o.value,i)){if(o.children===l.children&&!ge.current){t=at(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=lt(-1,n&-n),s.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var d=a.pending;d===null?s.next=s:(s.next=d.next,d.next=s),a.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),Ci(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(E(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),Ci(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}de(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Rn(t,n),l=ze(l),r=r(l),t.flags|=1,de(e,t,r,n),t.child;case 14:return r=t.type,l=Ie(r,t.pendingProps),l=Ie(r.type,l),Qs(e,t,r,l,n);case 15:return sf(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ie(r,l),vl(e,t),t.tag=1,we(r)?(e=!0,Ol(t)):e=!1,Rn(t,n),$c(t,r,l),Ti(t,r,l,n),Ri(null,t,r,!0,e,n);case 19:return pf(e,t,n);case 22:return af(e,t,n)}throw Error(E(156,t.tag))};function Rf(e,t){return ec(e,t)}function oh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Fe(e,t,n,r){return new oh(e,t,n,r)}function Du(e){return e=e.prototype,!(!e||!e.isReactComponent)}function ih(e){if(typeof e=="function")return Du(e)?1:0;if(e!=null){if(e=e.$$typeof,e===nu)return 11;if(e===ru)return 14}return 2}function Pt(e,t){var n=e.alternate;return n===null?(n=Fe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function wl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Du(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case pn:return Yt(n.children,l,o,t);case tu:i=8,l|=8;break;case Zo:return e=Fe(12,n,t,l|2),e.elementType=Zo,e.lanes=o,e;case Jo:return e=Fe(13,n,t,l),e.elementType=Jo,e.lanes=o,e;case qo:return e=Fe(19,n,t,l),e.elementType=qo,e.lanes=o,e;case Da:return io(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case za:i=10;break e;case $a:i=9;break e;case nu:i=11;break e;case ru:i=14;break e;case gt:i=16,r=null;break e}throw Error(E(130,e==null?e:typeof e,""))}return t=Fe(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Yt(e,t,n,r){return e=Fe(7,e,r,t),e.lanes=n,e}function io(e,t,n,r){return e=Fe(22,e,r,t),e.elementType=Da,e.lanes=n,e.stateNode={isHidden:!1},e}function Bo(e,t,n){return e=Fe(6,e,null,t),e.lanes=n,e}function Uo(e,t,n){return t=Fe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function uh(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=ko(0),this.expirationTimes=ko(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=ko(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Iu(e,t,n,r,l,o,i,u,s){return e=new uh(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Fe(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ku(o),e}function sh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Ff)}catch(e){console.error(e)}}Ff(),La.exports=Ne;var Mf=La.exports;const Cn=Yl(Mf);var oa=Mf;Yo.createRoot=oa.createRoot,Yo.hydrateRoot=oa.hydrateRoot;var zf={exports:{}};/*! Copyright (c) 2018 Jed Watson. Licensed under the MIT License (MIT), see http://jedwatson.github.io/classnames -*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function ph(e){var t=mh(e,"string");return typeof t=="symbol"?t:String(t)}function mh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function hh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var d=arguments.length,m=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Ah(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Uh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function po(e,t){return y.useMemo(()=>Uh(e,t),[e,t])}function Bh(e){return e&&"setState"in e?Cn.findDOMNode(e):e??null}const Hh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const m=y.useRef(null),p=po(m,s),g=N=>{p(Bh(N))},w=N=>T=>{N&&m.current&&N(m.current,T)},k=y.useCallback(w(e),[e]),R=y.useCallback(w(t),[t]),f=y.useCallback(w(n),[n]),c=y.useCallback(w(r),[r]),h=y.useCallback(w(l),[l]),S=y.useCallback(w(o),[o]),C=y.useCallback(w(i),[i]);return v.jsx(Mh,{ref:d,...a,onEnter:k,onEntered:f,onEntering:R,onExit:c,onExited:S,onExiting:h,addEndListener:C,nodeRef:m,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Wt.cloneElement(u,{ref:g})})}),Wh=Hh;function Vh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Vh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Vf=e=>y.forwardRef((t,n)=>v.jsx("div",{...t,ref:n,className:M(t.className,e)})),Qf=Vf("h4");Qf.displayName="DivStyledAsH4";const Kf=y.forwardRef(({className:e,bsPrefix:t,as:n=Qf,...r},l)=>(t=H(t,"alert-heading"),v.jsx(n,{ref:l,className:M(e,t),...r})));Kf.displayName="AlertHeading";const Qh=Kf;function Kh(){return y.useState(null)}function Gh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Yh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Xh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Zh=typeof document<"u",ca=Zh||Xh?y.useLayoutEffect:y.useEffect,Jh=["as","disabled"];function qh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function bh(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&bh(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},m=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:m},a]}const ev=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=qh(e,Jh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return v.jsx(i,Object.assign({},l,o,{ref:t}))});ev.displayName="Button";const tv=["onKeyDown"];function nv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function rv(e){return!e||e.trim()==="#"}const Gf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=nv(e,tv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return rv(r.href)||r.role==="button"?v.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):v.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Gf.displayName="Anchor";const lv=Gf,Yf=y.forwardRef(({className:e,bsPrefix:t,as:n=lv,...r},l)=>(t=H(t,"alert-link"),v.jsx(n,{ref:l,className:M(e,t),...r})));Yf.displayName="AlertLink";const ov=Yf,iv={[St]:"show",[Ht]:"show"},Xf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Ah(s),r==null||r(s,a)},[r]);return v.jsx(Wh,{ref:o,addEndListener:Ih,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,iv[s],n[s])})})});Xf.displayName="Fade";const Ql=Xf,uv={"aria-label":ot.string,onClick:ot.func,variant:ot.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>v.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=uv;const Zf=Wu,Jf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:m=Ql,...p}=vh(e,{show:"onClose"}),g=H(n,"alert"),w=Pe(f=>{a&&a(!1,f)}),k=m===!0?Ql:m,R=v.jsxs("div",{role:"alert",...k?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&v.jsx(Zf,{onClick:w,"aria-label":l,variant:o}),u]});return k?v.jsx(k,{unmountOnExit:!0,...p,ref:void 0,in:r,children:R}):r?R:null});Jf.displayName="Alert";const fa=Object.assign(Jf,{Link:ov,Heading:Qh}),qf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:m}]=Hu({tagName:e,disabled:o,...u}),p=m;return v.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});qf.displayName="Button";const Mr=qf;function sv(e){const t=y.useRef(e);return t.current=e,t}function bf(e){const t=sv(e);y.useEffect(()=>()=>t.current(),[])}function av(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function cv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function fv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=$f(),o=Df(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,m,p;typeof a=="object"&&a!=null?{span:d,offset:m,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),m!=null&&u.push(`offset${g}-${m}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const ed=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=fv(e);return v.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});ed.displayName="Col";const Vu=ed,td=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return v.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});td.displayName="Container";const dv=td;var pv=Function.prototype.bind.call(Function.prototype.call,[].slice);function cn(e,t){return pv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const mv="data-rr-ui-";function hv(e){return`${mv}${e}`}const nd=y.createContext(Wn?window:void 0);nd.Provider;function Qu(){return y.useContext(nd)}const vv={type:ot.string,tooltip:ot.bool,as:ot.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>v.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=vv;const rd=Ku,yv=y.createContext({}),ct=yv,ld=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ct);return t=H(t,"form-check-input"),v.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});ld.displayName="FormCheckInput";const od=ld,id=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-check-label"),v.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});id.displayName="FormCheckLabel";const Ki=id,ud=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:m,style:p,title:g="",type:w="checkbox",label:k,children:R,as:f="input",...c},h)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ct),C=y.useMemo(()=>({controlId:e||S}),[S,e]),N=!R&&k!=null&&k!==!1||cv(R,Ki),T=v.jsx(od,{...c,type:w==="switch"?"checkbox":w,ref:h,isValid:i,isInvalid:u,disabled:o,as:f});return v.jsx(ct.Provider,{value:C,children:v.jsx("div",{style:p,className:M(m,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,w==="switch"&&n),children:R||v.jsxs(v.Fragment,{children:[T,N&&v.jsx(Ki,{title:g,children:k}),a&&v.jsx(rd,{type:d,tooltip:s,children:a})]})})})});ud.displayName="FormCheck";const Kl=Object.assign(ud,{Input:od,Label:Ki}),sd=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...m},p)=>{const{controlId:g}=y.useContext(ct);return e=H(e,"form-control"),v.jsx(d,{...m,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});sd.displayName="FormControl";const gv=Object.assign(sd,{Feedback:rd}),ad=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),v.jsx(n,{ref:l,className:M(e,t),...r})));ad.displayName="FormFloating";const wv=ad,cd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return v.jsx(ct.Provider,{value:l,children:v.jsx(t,{...n,ref:r})})});cd.displayName="FormGroup";const fd=cd,dd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ct);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?v.jsx(Vu,{ref:u,as:"label",className:d,htmlFor:o,...i}):v.jsx(e,{ref:u,className:d,htmlFor:o,...i})});dd.displayName="FormLabel";const Sv=dd,pd=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-range"),v.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});pd.displayName="FormRange";const kv=pd,md=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ct);return e=H(e,"form-select"),v.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});md.displayName="FormSelect";const Ev=md,hd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),v.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));hd.displayName="FormText";const xv=hd,vd=y.forwardRef((e,t)=>v.jsx(Kl,{...e,ref:t,type:"switch"}));vd.displayName="Switch";const Cv=Object.assign(vd,{Input:Kl.Input,Label:Kl.Label}),yd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),v.jsxs(fd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,v.jsx("label",{htmlFor:r,children:l})]})));yd.displayName="FloatingLabel";const Nv=yd,Tv={_ref:ot.any,validated:ot.bool,as:ot.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>v.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=Tv;const On=Object.assign(Gu,{Group:fd,Control:gv,Floating:wv,Check:Kl,Switch:Cv,Label:Sv,Text:xv,Range:kv,Select:Ev,FloatingLabel:Nv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Wn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Ho(e){e===void 0&&(e=fo());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function _v(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=hv("modal-open");class jv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return _v(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=jv,Wo=(e,t)=>Wn?e==null?(t||fo()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Rv(e,t){const n=Qu(),[r,l]=y.useState(()=>Wo(e,n==null?void 0:n.document));if(!r){const o=Wo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Wo(e);o!==r&&l(o)},[e,r]),r}function Lv({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Pe(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=po(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Ov({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Pe(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Pv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Ov({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,m=>{throw a.in||i(!0),m})}}),s=po(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?v.jsx(e,Object.assign({},n)):t?v.jsx(Pv,Object.assign({},n,{transition:t})):v.jsx(Lv,Object.assign({},n))}function Fv(e){return e.code==="Escape"||e.keyCode===27}const Mv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function zv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Vo;function $v(e){return Vo||(Vo=new Yu({ownerDocument:e==null?void 0:e.document})),Vo}function Dv(e){const t=Qu(),n=e||$v(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const gd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:m,runTransition:p,backdropTransition:g,runBackdropTransition:w,autoFocus:k=!0,enforceFocus:R=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:h,renderBackdrop:S=K=>v.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:j=()=>{},onExit:U,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:rn}=e,Qn=zv(e,Mv);const _e=Qu(),Ke=Rv(N),x=Dv(C),L=Gh(),O=Yh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>x,[x]),Wn&&!O&&n&&(fe.current=Ho(_e==null?void 0:_e.document)),n&&D&&A(!1);const je=Pe(()=>{if(x.add(),on.current=Vl(document,"keydown",mo),ln.current=Vl(document,"focus",()=>setTimeout(Re),!0),T&&T(),k){var K,Hr;const Yn=Ho((K=(Hr=x.dialog)==null?void 0:Hr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Yn&&!da(x.dialog,Yn)&&(fe.current=Yn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),on.current==null||on.current(),ln.current==null||ln.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ke||je()},[n,Ke,je]),y.useEffect(()=>{D&&qe()},[D,qe]),bf(()=>{qe()});const Re=Pe(()=>{if(!R||!L()||!x.isTopModal())return;const K=Ho(_e==null?void 0:_e.document);x.dialog&&K&&!da(x.dialog,K)&&x.dialog.focus()}),mt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),mo=Pe(K=>{s&&Fv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||j())}),ln=y.useRef(),on=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const Br=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=h?h(Br):v.jsx("div",Object.assign({},Br,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(m,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:U,onExiting:ie,onExited:Kn,onEnter:Ve,onEntering:Qe,onEntered:rn,children:Gn});let It=null;return u&&(It=S({ref:x.setBackdropRef,onClick:mt}),It=ha(g,w,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),v.jsx(v.Fragment,{children:Cn.createPortal(v.jsxs(v.Fragment,{children:[It,Gn]}),Ke)})});gd.displayName="Modal";const Iv=Object.assign(gd,{Manager:Yu});function Av(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Uv(e,t){e.classList?e.classList.add(t):Av(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Bv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const fn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Hv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Uv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Bv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Qo;function Wv(e){return Qo||(Qo=new Hv(e)),Qo}const wd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),v.jsx(n,{ref:l,className:M(e,t),...r})));wd.displayName="ModalBody";const Vv=wd,Qv=y.createContext({onHide(){}}),Sd=Qv,kd=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,m=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return v.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&m),children:v.jsx("div",{className:M(`${e}-content`,n),children:i})})});kd.displayName="ModalDialog";const Ed=kd,xd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),v.jsx(n,{ref:l,className:M(e,t),...r})));xd.displayName="ModalFooter";const Kv=xd,Gv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(Sd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return v.jsxs("div",{ref:i,...o,children:[l,n&&v.jsx(Zf,{"aria-label":e,variant:t,onClick:s})]})}),Yv=Gv,Cd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),v.jsx(Yv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Cd.displayName="ModalHeader";const Xv=Cd,Zv=Vf("h4"),Nd=y.forwardRef(({className:e,bsPrefix:t,as:n=Zv,...r},l)=>(t=H(t,"modal-title"),v.jsx(n,{ref:l,className:M(e,t),...r})));Nd.displayName="ModalTitle";const Jv=Nd;function qv(e){return v.jsx(Ql,{...e,timeout:null})}function bv(e){return v.jsx(Ql,{...e,timeout:null})}const Td=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=Ed,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:m=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:w,onShow:k,onHide:R,container:f,autoFocus:c=!0,enforceFocus:h=!0,restoreFocus:S=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:j,onEnter:U,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...rn},Qn)=>{const[_e,Ke]=y.useState({}),[x,L]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,je]=Kh(),qe=po(Qn,je),Re=Pe(R),mt=Sh();e=H(e,"modal");const mo=y.useMemo(()=>({onHide:Re}),[Re]);function ln(){return Qe||Wv({isRTL:mt})}function on($){if(!Wn)return;const un=ln().getScrollbarWidth()>0,Zu=$.scrollHeight>fo($).documentElement.clientHeight;Ke({paddingRight:un&&!Zu?pa():void 0,paddingLeft:!un&&Zu?pa():void 0})}const Kn=Pe(()=>{fe&&on(fe.dialog)});bf(()=>{Qi(window,"resize",Kn),A.current==null||A.current()});const Br=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{L(!0),A.current=Wf(fe.dialog,()=>{L(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}R==null||R()},Yn=$=>{g?w==null||w($):($.preventDefault(),p==="static"&&It())},$d=($,un)=>{$&&on($),U==null||U($,un)},Dd=$=>{A.current==null||A.current(),T==null||T($)},Id=($,un)=>{P==null||P($,un),Hf(window,"resize",Kn)},Ad=$=>{$&&($.style.display=""),ie==null||ie($),Qi(window,"resize",Kn)},Ud=y.useCallback($=>v.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!m&&"show")}),[m,Ve,e]),Xu={...n,..._e};Xu.display="block";const Bd=$=>v.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,x&&`${e}-static`,!m&&"show"),onClick:p?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:v.jsx(i,{...rn,onMouseDown:Br,className:r,contentClassName:l,children:o})});return v.jsx(Sd.Provider,{value:mo,children:v.jsx(Iv,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:h,restoreFocus:S,restoreFocusOptions:C,onEscapeKeyDown:Yn,onShow:k,onHide:R,onEnter:$d,onEntering:Id,onEntered:N,onExit:Dd,onExiting:j,onExited:Ad,manager:ln(),transition:m?qv:void 0,backdropTransition:m?bv:void 0,renderBackdrop:Ud,renderDialog:Bd})})});Td.displayName="Modal";const tt=Object.assign(Td,{Body:Vv,Header:Xv,Title:Jv,Footer:Kv,Dialog:Ed,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ey(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...m},p){return v.jsx("div",{ref:p,...m,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${ey(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?v.jsx("span",{className:"visually-hidden",children:r}):r})}const _d=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p,className:g,children:w,...k}=r;return v.jsx("div",{ref:n,...k,className:M(g,m),children:w?av(w,R=>y.cloneElement(R,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p},n)})});_d.displayName="ProgressBar";const ty=_d,jd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=$f(),u=Df(),s=`${o}-cols`,a=[];return i.forEach(d=>{const m=r[d];delete r[d];let p;m!=null&&typeof m=="object"?{cols:p}=m:p=m;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),v.jsx(n,{ref:l,...r,className:M(t,o,...a)})});jd.displayName="Row";const Rd=jd,Ld=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return v.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Ld.displayName="Spinner";const An=Ld,ny=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",Gi="initializing",ry="paused",Od="live",vt=async(e,t,n)=>{console.log(e,t);const r=ny+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ft={listTorrents:()=>vt("GET","/torrents"),getTorrentDetails:e=>vt("GET",`/torrents/${e}`),getTorrentStats:e=>vt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),vt("POST",n,e)},pause:e=>vt("POST",`/torrents/${e}/pause`),start:e=>vt("POST",`/torrents/${e}/start`),forget:e=>vt("POST",`/torrents/${e}/forget`),delete:e=>vt("POST",`/torrents/${e}/delete`)},Vn=y.createContext(null),Pd=y.createContext(null),Ko=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return v.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},ly=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Vn),d=()=>{l(!1),i(null),s(!1),n()},m=()=>{s(!0),(r?ft.delete:ft.forget)(e).then(()=>{a.refreshTorrents(),d()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return v.jsxs(tt,{show:t,onHide:d,children:[v.jsx(tt.Header,{closeButton:!0,children:"Delete torrent"}),v.jsxs(tt.Body,{children:[v.jsx(On,{children:v.jsx(On.Group,{controlId:"delete-torrent",children:v.jsx(On.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&v.jsx(zr,{error:o})]}),v.jsxs(tt.Footer,{children:[u&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:m,disabled:u,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:d,children:"Cancel"})]})]})},oy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Pd);const s=n=="live",a=n=="paused",d=y.useContext(Vn),m=()=>{l(!0),ft.start(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error starting torrent id=${e}`,details:k})}).finally(()=>l(!1))},p=()=>{l(!0),ft.pause(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error pausing torrent id=${e}`,details:k})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},w=()=>{l(!1),i(!1)};return v.jsx(Rd,{children:v.jsxs(Vu,{children:[a&&v.jsx(Ko,{className:"bi-play-circle",onClick:m,disabled:r,color:"success"}),s&&v.jsx(Ko,{className:"bi-pause-circle",onClick:p,disabled:r}),v.jsx(Ko,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),v.jsx(ly,{id:e,show:o,onHide:w})]})})},iy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Gi||r==Od)&&!u,d=l?"Error":`${s.toFixed(2)}%`,m=l?"danger":u?"success":"primary",p=()=>{var R;let k=(R=n==null?void 0:n.live)==null?void 0:R.snapshot.peer_stats;return k?`${k.live} / ${k.seen}`:""},g=()=>{var k;return u?"Completed":r==Gi?"Checking files":((k=n.live)==null?void 0:k.download_speed.human_readable)??"N/A"};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),v.jsxs(Rd,{className:w.join(" "),children:[v.jsx(yt,{size:3,label:"Name",children:t?v.jsxs(v.Fragment,{children:[v.jsx("div",{className:"text-truncate",children:vy(t)}),l&&v.jsxs("p",{className:"text-danger",children:[v.jsx("strong",{children:"Error:"})," ",l]})]}):v.jsx(An,{})}),n?v.jsxs(v.Fragment,{children:[v.jsx(yt,{label:"Size",children:`${Md(o)} `}),v.jsx(yt,{size:2,label:r==ry?"Progress (PAUSED)":"Progress",children:v.jsx(ty,{now:s,label:d,animated:a,variant:m})}),v.jsx(yt,{size:2,label:"Down Speed",children:g()}),v.jsx(yt,{label:"ETA",children:yy(n)}),v.jsx(yt,{size:2,label:"Peers",children:p()}),v.jsx(yt,{label:"Actions",children:v.jsx(oy,{id:e,statsResponse:n})})]}):v.jsx(yt,{label:"Loading stats",size:8,children:v.jsx(An,{})})]})},yt=({size:e,label:t,children:n})=>v.jsxs(Vu,{md:e||1,className:"py-3",children:[v.jsx("div",{className:"fw-bold",children:t}),n]}),uy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(0),s=()=>{u(i+1)};return y.useEffect(()=>{if(n===null)return wy(async()=>{await ft.getTorrentDetails(t.id).then(r)},1e3)},[n]),y.useEffect(()=>zd(async()=>ft.getTorrentStats(t.id).then(g=>(o(g),g)).then(g=>g.finished?1e4:g.state==Gi||g.state==Od?1e3:1e4,g=>1e4),0),[i]),v.jsx(Pd.Provider,{value:{refresh:s},children:v.jsx(iy,{id:e,detailsResponse:n,statsResponse:l})})},sy=e=>{if(e.torrents===null&&e.loading)return v.jsx(An,{});if(e.torrents!==null)return e.torrents.length===0?v.jsx("div",{className:"text-center",children:v.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):v.jsx(v.Fragment,{children:e.torrents.map(t=>v.jsx(uy,{id:t.id,torrent:t},t.id))})},ay=()=>{const[e,t]=y.useState(null),[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(!1),s=async()=>{u(!0);let d=await ft.listTorrents().finally(()=>u(!1));o(d.torrents)};y.useEffect(()=>zd(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return v.jsx(Vn.Provider,{value:a,children:v.jsxs("div",{className:"text-center",children:[v.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 0.0.1-alpha"}),v.jsx(hy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},cy=e=>{let{details:t}=e;return t?v.jsxs(v.Fragment,{children:[t.status&&v.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},zr=e=>{let{error:t,remove:n}=e;return t==null?null:v.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[v.jsx(fa.Heading,{children:t.text}),v.jsx(cy,{details:t.details})]})},Fd=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState([]),[a,d]=y.useState(null);y.useContext(Vn);const m=n!==null||a!==null;y.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const w=await ft.uploadTorrent(n,{listOnly:!0});s(w.details.files)}catch(w){d({text:"Error listing torrent",details:w})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return v.jsxs(v.Fragment,{children:[v.jsx(Mr,{variant:l,onClick:t,className:"m-1",children:e}),v.jsx(py,{show:m,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},fy=()=>{let[e,t]=y.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return v.jsx(Fd,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},dy=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return v.jsxs(v.Fragment,{children:[v.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),v.jsx(Fd,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},py=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=y.useState([]),[a,d]=y.useState(!1),[m,p]=y.useState(null),g=y.useContext(Vn);y.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const w=()=>{n(),s([]),p(null),d(!1)},k=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},R=async()=>{d(!0),ft.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return v.jsxs(tt,{show:t,onHide:w,size:"lg",children:[v.jsx(tt.Header,{closeButton:!0,children:!!l||v.jsx(tt.Title,{children:"Select Files"})}),v.jsxs(tt.Body,{children:[o?v.jsx(An,{}):l?v.jsx(zr,{error:l}):v.jsx(On,{children:r.map((f,c)=>v.jsx(On.Group,{controlId:`check-${c}`,children:v.jsx(On.Check,{type:"checkbox",label:`${f.name} (${Md(f.length)})`,checked:u.includes(c),onChange:()=>k(c)})},c))}),v.jsx(zr,{error:m})]}),v.jsxs(tt.Footer,{children:[a&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:R,disabled:o||a||u.length==0,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:w,children:"Cancel"})]})]})},my=()=>v.jsxs("div",{id:"buttons-container",className:"mt-3",children:[v.jsx(fy,{}),v.jsx(dy,{})]}),hy=e=>{let t=y.useContext(Vn);return v.jsxs(dv,{children:[v.jsx(zr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),v.jsx(zr,{error:e.otherError}),v.jsx(sy,{torrents:e.torrents,loading:e.torrentsLoading}),v.jsx(my,{})]})};function Md(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function vy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function yy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":gy(t)}function gy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function zd(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function wy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function Sy(){const e=document.getElementById("app");Go.createRoot(e).render(v.jsx(y.StrictMode,{children:v.jsx(ay,{})}))}document.addEventListener("DOMContentLoaded",Sy); +*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function mh(e){var t=hh(e,"string");return typeof t=="symbol"?t:String(t)}function hh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function vh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var d=arguments.length,m=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Bh(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Uh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function mo(e,t){return y.useMemo(()=>Uh(e,t),[e,t])}function Hh(e){return e&&"setState"in e?Cn.findDOMNode(e):e??null}const Wh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const m=y.useRef(null),p=mo(m,s),g=N=>{p(Hh(N))},w=N=>T=>{N&&m.current&&N(m.current,T)},k=y.useCallback(w(e),[e]),R=y.useCallback(w(t),[t]),f=y.useCallback(w(n),[n]),c=y.useCallback(w(r),[r]),h=y.useCallback(w(l),[l]),S=y.useCallback(w(o),[o]),C=y.useCallback(w(i),[i]);return v.jsx(zh,{ref:d,...a,onEnter:k,onEntered:f,onEntering:R,onExit:c,onExited:S,onExiting:h,addEndListener:C,nodeRef:m,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Wt.cloneElement(u,{ref:g})})}),Vh=Wh;function Qh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Qh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Qf=e=>y.forwardRef((t,n)=>v.jsx("div",{...t,ref:n,className:M(t.className,e)})),Kf=Qf("h4");Kf.displayName="DivStyledAsH4";const Gf=y.forwardRef(({className:e,bsPrefix:t,as:n=Kf,...r},l)=>(t=H(t,"alert-heading"),v.jsx(n,{ref:l,className:M(e,t),...r})));Gf.displayName="AlertHeading";const Kh=Gf;function Gh(){return y.useState(null)}function Yh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Xh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Zh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Jh=typeof document<"u",ca=Jh||Zh?y.useLayoutEffect:y.useEffect,qh=["as","disabled"];function bh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function ev(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&ev(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},m=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:m},a]}const tv=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=bh(e,qh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return v.jsx(i,Object.assign({},l,o,{ref:t}))});tv.displayName="Button";const nv=["onKeyDown"];function rv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function lv(e){return!e||e.trim()==="#"}const Yf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=rv(e,nv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return lv(r.href)||r.role==="button"?v.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):v.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Yf.displayName="Anchor";const ov=Yf,Xf=y.forwardRef(({className:e,bsPrefix:t,as:n=ov,...r},l)=>(t=H(t,"alert-link"),v.jsx(n,{ref:l,className:M(e,t),...r})));Xf.displayName="AlertLink";const iv=Xf,uv={[St]:"show",[Ht]:"show"},Zf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Bh(s),r==null||r(s,a)},[r]);return v.jsx(Vh,{ref:o,addEndListener:Ah,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,uv[s],n[s])})})});Zf.displayName="Fade";const Kl=Zf,sv={"aria-label":ot.string,onClick:ot.func,variant:ot.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>v.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=sv;const Jf=Wu,qf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:m=Kl,...p}=yh(e,{show:"onClose"}),g=H(n,"alert"),w=Pe(f=>{a&&a(!1,f)}),k=m===!0?Kl:m,R=v.jsxs("div",{role:"alert",...k?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&v.jsx(Jf,{onClick:w,"aria-label":l,variant:o}),u]});return k?v.jsx(k,{unmountOnExit:!0,...p,ref:void 0,in:r,children:R}):r?R:null});qf.displayName="Alert";const fa=Object.assign(qf,{Link:iv,Heading:Kh}),bf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:m}]=Hu({tagName:e,disabled:o,...u}),p=m;return v.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});bf.displayName="Button";const Mr=bf;function av(e){const t=y.useRef(e);return t.current=e,t}function ed(e){const t=av(e);y.useEffect(()=>()=>t.current(),[])}function cv(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function fv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function dv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=Df(),o=If(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,m,p;typeof a=="object"&&a!=null?{span:d,offset:m,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),m!=null&&u.push(`offset${g}-${m}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const td=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=dv(e);return v.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});td.displayName="Col";const Vu=td,nd=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return v.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});nd.displayName="Container";const pv=nd;var mv=Function.prototype.bind.call(Function.prototype.call,[].slice);function cn(e,t){return mv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const hv="data-rr-ui-";function vv(e){return`${hv}${e}`}const rd=y.createContext(Wn?window:void 0);rd.Provider;function Qu(){return y.useContext(rd)}const yv={type:ot.string,tooltip:ot.bool,as:ot.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>v.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=yv;const ld=Ku,gv=y.createContext({}),ct=gv,od=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ct);return t=H(t,"form-check-input"),v.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});od.displayName="FormCheckInput";const id=od,ud=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-check-label"),v.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});ud.displayName="FormCheckLabel";const Gi=ud,sd=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:m,style:p,title:g="",type:w="checkbox",label:k,children:R,as:f="input",...c},h)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ct),C=y.useMemo(()=>({controlId:e||S}),[S,e]),N=!R&&k!=null&&k!==!1||fv(R,Gi),T=v.jsx(id,{...c,type:w==="switch"?"checkbox":w,ref:h,isValid:i,isInvalid:u,disabled:o,as:f});return v.jsx(ct.Provider,{value:C,children:v.jsx("div",{style:p,className:M(m,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,w==="switch"&&n),children:R||v.jsxs(v.Fragment,{children:[T,N&&v.jsx(Gi,{title:g,children:k}),a&&v.jsx(ld,{type:d,tooltip:s,children:a})]})})})});sd.displayName="FormCheck";const Gl=Object.assign(sd,{Input:id,Label:Gi}),ad=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...m},p)=>{const{controlId:g}=y.useContext(ct);return e=H(e,"form-control"),v.jsx(d,{...m,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});ad.displayName="FormControl";const wv=Object.assign(ad,{Feedback:ld}),cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),v.jsx(n,{ref:l,className:M(e,t),...r})));cd.displayName="FormFloating";const Sv=cd,fd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return v.jsx(ct.Provider,{value:l,children:v.jsx(t,{...n,ref:r})})});fd.displayName="FormGroup";const dd=fd,pd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ct);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?v.jsx(Vu,{ref:u,as:"label",className:d,htmlFor:o,...i}):v.jsx(e,{ref:u,className:d,htmlFor:o,...i})});pd.displayName="FormLabel";const kv=pd,md=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-range"),v.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});md.displayName="FormRange";const Ev=md,hd=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ct);return e=H(e,"form-select"),v.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});hd.displayName="FormSelect";const xv=hd,vd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),v.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));vd.displayName="FormText";const Cv=vd,yd=y.forwardRef((e,t)=>v.jsx(Gl,{...e,ref:t,type:"switch"}));yd.displayName="Switch";const Nv=Object.assign(yd,{Input:Gl.Input,Label:Gl.Label}),gd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),v.jsxs(dd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,v.jsx("label",{htmlFor:r,children:l})]})));gd.displayName="FloatingLabel";const Tv=gd,_v={_ref:ot.any,validated:ot.bool,as:ot.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>v.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=_v;const On=Object.assign(Gu,{Group:dd,Control:wv,Floating:Sv,Check:Gl,Switch:Nv,Label:kv,Text:Cv,Range:Ev,Select:xv,FloatingLabel:Tv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Wn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Wo(e){e===void 0&&(e=po());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function jv(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=vv("modal-open");class Rv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return jv(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=Rv,Vo=(e,t)=>Wn?e==null?(t||po()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Lv(e,t){const n=Qu(),[r,l]=y.useState(()=>Vo(e,n==null?void 0:n.document));if(!r){const o=Vo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Vo(e);o!==r&&l(o)},[e,r]),r}function Ov({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Pe(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=mo(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Pv({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Pe(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Fv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Pv({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,m=>{throw a.in||i(!0),m})}}),s=mo(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?v.jsx(e,Object.assign({},n)):t?v.jsx(Fv,Object.assign({},n,{transition:t})):v.jsx(Ov,Object.assign({},n))}function Mv(e){return e.code==="Escape"||e.keyCode===27}const zv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function $v(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Qo;function Dv(e){return Qo||(Qo=new Yu({ownerDocument:e==null?void 0:e.document})),Qo}function Iv(e){const t=Qu(),n=e||Dv(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const wd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:m,runTransition:p,backdropTransition:g,runBackdropTransition:w,autoFocus:k=!0,enforceFocus:R=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:h,renderBackdrop:S=K=>v.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:j=()=>{},onExit:B,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:rn}=e,Qn=$v(e,zv);const _e=Qu(),Ke=Lv(N),x=Iv(C),L=Yh(),O=Xh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>x,[x]),Wn&&!O&&n&&(fe.current=Wo(_e==null?void 0:_e.document)),n&&D&&A(!1);const je=Pe(()=>{if(x.add(),on.current=Ql(document,"keydown",ho),ln.current=Ql(document,"focus",()=>setTimeout(Re),!0),T&&T(),k){var K,Hr;const Yn=Wo((K=(Hr=x.dialog)==null?void 0:Hr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Yn&&!da(x.dialog,Yn)&&(fe.current=Yn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),on.current==null||on.current(),ln.current==null||ln.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ke||je()},[n,Ke,je]),y.useEffect(()=>{D&&qe()},[D,qe]),ed(()=>{qe()});const Re=Pe(()=>{if(!R||!L()||!x.isTopModal())return;const K=Wo(_e==null?void 0:_e.document);x.dialog&&K&&!da(x.dialog,K)&&x.dialog.focus()}),mt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),ho=Pe(K=>{s&&Mv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||j())}),ln=y.useRef(),on=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const Ur=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=h?h(Ur):v.jsx("div",Object.assign({},Ur,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(m,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:B,onExiting:ie,onExited:Kn,onEnter:Ve,onEntering:Qe,onEntered:rn,children:Gn});let It=null;return u&&(It=S({ref:x.setBackdropRef,onClick:mt}),It=ha(g,w,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),v.jsx(v.Fragment,{children:Cn.createPortal(v.jsxs(v.Fragment,{children:[It,Gn]}),Ke)})});wd.displayName="Modal";const Av=Object.assign(wd,{Manager:Yu});function Bv(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Uv(e,t){e.classList?e.classList.add(t):Bv(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Hv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const fn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Wv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Uv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Hv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Ko;function Vv(e){return Ko||(Ko=new Wv(e)),Ko}const Sd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),v.jsx(n,{ref:l,className:M(e,t),...r})));Sd.displayName="ModalBody";const Qv=Sd,Kv=y.createContext({onHide(){}}),kd=Kv,Ed=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,m=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return v.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&m),children:v.jsx("div",{className:M(`${e}-content`,n),children:i})})});Ed.displayName="ModalDialog";const xd=Ed,Cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),v.jsx(n,{ref:l,className:M(e,t),...r})));Cd.displayName="ModalFooter";const Gv=Cd,Yv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(kd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return v.jsxs("div",{ref:i,...o,children:[l,n&&v.jsx(Jf,{"aria-label":e,variant:t,onClick:s})]})}),Xv=Yv,Nd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),v.jsx(Xv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Nd.displayName="ModalHeader";const Zv=Nd,Jv=Qf("h4"),Td=y.forwardRef(({className:e,bsPrefix:t,as:n=Jv,...r},l)=>(t=H(t,"modal-title"),v.jsx(n,{ref:l,className:M(e,t),...r})));Td.displayName="ModalTitle";const qv=Td;function bv(e){return v.jsx(Kl,{...e,timeout:null})}function ey(e){return v.jsx(Kl,{...e,timeout:null})}const _d=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=xd,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:m=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:w,onShow:k,onHide:R,container:f,autoFocus:c=!0,enforceFocus:h=!0,restoreFocus:S=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:j,onEnter:B,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...rn},Qn)=>{const[_e,Ke]=y.useState({}),[x,L]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,je]=Gh(),qe=mo(Qn,je),Re=Pe(R),mt=kh();e=H(e,"modal");const ho=y.useMemo(()=>({onHide:Re}),[Re]);function ln(){return Qe||Vv({isRTL:mt})}function on($){if(!Wn)return;const un=ln().getScrollbarWidth()>0,Zu=$.scrollHeight>po($).documentElement.clientHeight;Ke({paddingRight:un&&!Zu?pa():void 0,paddingLeft:!un&&Zu?pa():void 0})}const Kn=Pe(()=>{fe&&on(fe.dialog)});ed(()=>{Ki(window,"resize",Kn),A.current==null||A.current()});const Ur=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{L(!0),A.current=Vf(fe.dialog,()=>{L(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}R==null||R()},Yn=$=>{g?w==null||w($):($.preventDefault(),p==="static"&&It())},Dd=($,un)=>{$&&on($),B==null||B($,un)},Id=$=>{A.current==null||A.current(),T==null||T($)},Ad=($,un)=>{P==null||P($,un),Wf(window,"resize",Kn)},Bd=$=>{$&&($.style.display=""),ie==null||ie($),Ki(window,"resize",Kn)},Ud=y.useCallback($=>v.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!m&&"show")}),[m,Ve,e]),Xu={...n,..._e};Xu.display="block";const Hd=$=>v.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,x&&`${e}-static`,!m&&"show"),onClick:p?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:v.jsx(i,{...rn,onMouseDown:Ur,className:r,contentClassName:l,children:o})});return v.jsx(kd.Provider,{value:ho,children:v.jsx(Av,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:h,restoreFocus:S,restoreFocusOptions:C,onEscapeKeyDown:Yn,onShow:k,onHide:R,onEnter:Dd,onEntering:Ad,onEntered:N,onExit:Id,onExiting:j,onExited:Bd,manager:ln(),transition:m?bv:void 0,backdropTransition:m?ey:void 0,renderBackdrop:Ud,renderDialog:Hd})})});_d.displayName="Modal";const tt=Object.assign(_d,{Body:Qv,Header:Zv,Title:qv,Footer:Gv,Dialog:xd,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ty(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...m},p){return v.jsx("div",{ref:p,...m,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${ty(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?v.jsx("span",{className:"visually-hidden",children:r}):r})}const jd=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p,className:g,children:w,...k}=r;return v.jsx("div",{ref:n,...k,className:M(g,m),children:w?cv(w,R=>y.cloneElement(R,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p},n)})});jd.displayName="ProgressBar";const ny=jd,Rd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=Df(),u=If(),s=`${o}-cols`,a=[];return i.forEach(d=>{const m=r[d];delete r[d];let p;m!=null&&typeof m=="object"?{cols:p}=m:p=m;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),v.jsx(n,{ref:l,...r,className:M(t,o,...a)})});Rd.displayName="Row";const Ld=Rd,Od=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return v.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Od.displayName="Spinner";const An=Od,ry=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",Sl="initializing",wa="paused",Pd="live",ly="error",vt=async(e,t,n)=>{console.log(e,t);const r=ry+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ft={listTorrents:()=>vt("GET","/torrents"),getTorrentDetails:e=>vt("GET",`/torrents/${e}`),getTorrentStats:e=>vt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),vt("POST",n,e)},pause:e=>vt("POST",`/torrents/${e}/pause`),start:e=>vt("POST",`/torrents/${e}/start`),forget:e=>vt("POST",`/torrents/${e}/forget`),delete:e=>vt("POST",`/torrents/${e}/delete`)},Vn=y.createContext(null),Fd=y.createContext(null),Go=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return v.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},oy=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Vn),d=()=>{l(!1),i(null),s(!1),n()},m=()=>{s(!0),(r?ft.delete:ft.forget)(e).then(()=>{a.refreshTorrents(),d()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return v.jsxs(tt,{show:t,onHide:d,children:[v.jsx(tt.Header,{closeButton:!0,children:"Delete torrent"}),v.jsxs(tt.Body,{children:[v.jsx(On,{children:v.jsx(On.Group,{controlId:"delete-torrent",children:v.jsx(On.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&v.jsx(zr,{error:o})]}),v.jsxs(tt.Footer,{children:[u&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:m,disabled:u,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:d,children:"Cancel"})]})]})},iy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Fd);const s=n=="live",a=n=="paused",d=y.useContext(Vn),m=()=>{l(!0),ft.start(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error starting torrent id=${e}`,details:k})}).finally(()=>l(!1))},p=()=>{l(!0),ft.pause(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error pausing torrent id=${e}`,details:k})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},w=()=>{l(!1),i(!1)};return v.jsx(Ld,{children:v.jsxs(Vu,{children:[a&&v.jsx(Go,{className:"bi-play-circle",onClick:m,disabled:r,color:"success"}),s&&v.jsx(Go,{className:"bi-pause-circle",onClick:p,disabled:r}),v.jsx(Go,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),v.jsx(oy,{id:e,show:o,onHide:w})]})})},uy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Sl||r==Pd)&&!u,d=l?"Error":`${s.toFixed(2)}%`,m=l?"danger":u?"success":r==Sl?"warning":"primary",p=()=>{var R;let k=(R=n==null?void 0:n.live)==null?void 0:R.snapshot.peer_stats;return k?`${k.live} / ${k.seen}`:""},g=()=>{var k;if(u)return"Completed";switch(r){case wa:return"Paused";case Sl:return"Checking files";case ly:return"Error"}return((k=n.live)==null?void 0:k.download_speed.human_readable)??"N/A"};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),v.jsxs(Ld,{className:w.join(" "),children:[v.jsx(yt,{size:3,label:"Name",children:t?v.jsxs(v.Fragment,{children:[v.jsx("div",{className:"text-truncate",children:yy(t)}),l&&v.jsxs("p",{className:"text-danger",children:[v.jsx("strong",{children:"Error:"})," ",l]})]}):v.jsx(An,{})}),n?v.jsxs(v.Fragment,{children:[v.jsx(yt,{label:"Size",children:`${zd(o)} `}),v.jsx(yt,{size:2,label:(r==wa,"Progress"),children:v.jsx(ny,{now:s,label:d,animated:a,variant:m})}),v.jsx(yt,{size:2,label:"Down Speed",children:g()}),v.jsx(yt,{label:"ETA",children:gy(n)}),v.jsx(yt,{size:2,label:"Peers",children:p()}),v.jsx(yt,{label:"Actions",children:v.jsx(iy,{id:e,statsResponse:n})})]}):v.jsx(yt,{label:"Loading stats",size:8,children:v.jsx(An,{})})]})},yt=({size:e,label:t,children:n})=>v.jsxs(Vu,{md:e||1,className:"py-3",children:[v.jsx("div",{className:"fw-bold",children:t}),n]}),sy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(0),s=()=>{u(i+1)};return y.useEffect(()=>{if(n===null)return Sy(async()=>{await ft.getTorrentDetails(t.id).then(r)},1e3)},[n]),y.useEffect(()=>$d(async()=>ft.getTorrentStats(t.id).then(g=>(o(g),g)).then(g=>g.finished?1e4:g.state==Sl||g.state==Pd?1e3:1e4,g=>1e4),0),[i]),v.jsx(Fd.Provider,{value:{refresh:s},children:v.jsx(uy,{id:e,detailsResponse:n,statsResponse:l})})},ay=e=>{if(e.torrents===null&&e.loading)return v.jsx(An,{});if(e.torrents!==null)return e.torrents.length===0?v.jsx("div",{className:"text-center",children:v.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):v.jsx(v.Fragment,{children:e.torrents.map(t=>v.jsx(sy,{id:t.id,torrent:t},t.id))})},cy=()=>{const[e,t]=y.useState(null),[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(!1),s=async()=>{u(!0);let d=await ft.listTorrents().finally(()=>u(!1));o(d.torrents)};y.useEffect(()=>$d(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return v.jsx(Vn.Provider,{value:a,children:v.jsxs("div",{className:"text-center",children:[v.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 0.0.1-alpha"}),v.jsx(vy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},fy=e=>{let{details:t}=e;return t?v.jsxs(v.Fragment,{children:[t.status&&v.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},zr=e=>{let{error:t,remove:n}=e;return t==null?null:v.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[v.jsx(fa.Heading,{children:t.text}),v.jsx(fy,{details:t.details})]})},Md=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState([]),[a,d]=y.useState(null);y.useContext(Vn);const m=n!==null||a!==null;y.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const w=await ft.uploadTorrent(n,{listOnly:!0});s(w.details.files)}catch(w){d({text:"Error uploading torrent",details:w})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return v.jsxs(v.Fragment,{children:[v.jsx(Mr,{variant:l,onClick:t,className:"m-1",children:e}),v.jsx(my,{show:m,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},dy=()=>{let[e,t]=y.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return v.jsx(Md,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},py=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return v.jsxs(v.Fragment,{children:[v.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),v.jsx(Md,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},my=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=y.useState([]),[a,d]=y.useState(!1),[m,p]=y.useState(null),g=y.useContext(Vn);y.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const w=()=>{n(),s([]),p(null),d(!1)},k=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},R=async()=>{d(!0),ft.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return v.jsxs(tt,{show:t,onHide:w,size:"lg",children:[v.jsx(tt.Header,{closeButton:!0,children:!!l||v.jsx(tt.Title,{children:"Select Files"})}),v.jsxs(tt.Body,{children:[o?v.jsx(An,{}):l?v.jsx(zr,{error:l}):v.jsx(On,{children:r.map((f,c)=>v.jsx(On.Group,{controlId:`check-${c}`,children:v.jsx(On.Check,{type:"checkbox",label:`${f.name} (${zd(f.length)})`,checked:u.includes(c),onChange:()=>k(c)})},c))}),v.jsx(zr,{error:m})]}),v.jsxs(tt.Footer,{children:[a&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:R,disabled:o||a||u.length==0,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:w,children:"Cancel"})]})]})},hy=()=>v.jsxs("div",{id:"buttons-container",className:"mt-3",children:[v.jsx(dy,{}),v.jsx(py,{})]}),vy=e=>{let t=y.useContext(Vn);return v.jsxs(pv,{children:[v.jsx(zr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),v.jsx(zr,{error:e.otherError}),v.jsx(ay,{torrents:e.torrents,loading:e.torrentsLoading}),v.jsx(hy,{})]})};function zd(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function yy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function gy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":wy(t)}function wy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function $d(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function Sy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function ky(){const e=document.getElementById("app");Yo.createRoot(e).render(v.jsx(y.StrictMode,{children:v.jsx(cy,{})}))}document.addEventListener("DOMContentLoaded",ky); diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index bdcff52..9462266 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -161,7 +161,7 @@ const TorrentRow: React.FC<{ const progressPercentage = error ? 100 : (progressBytes / totalBytes) * 100; const isAnimated = (state == STATE_INITIALIZING || state == STATE_LIVE) && !finished; const progressLabel = error ? 'Error' : `${progressPercentage.toFixed(2)}%`; - const progressBarVariant = error ? 'danger' : finished ? 'success' : 'primary'; + const progressBarVariant = error ? 'danger' : finished ? 'success' : state == STATE_INITIALIZING ? 'warning' : 'primary'; const formatPeersString = () => { let peer_stats = statsResponse?.live?.snapshot.peer_stats; From 6f113c5137ba194d8da7e1fd9f5444c4253f544a Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 13:46:50 +0000 Subject: [PATCH 30/38] Fatal error handling --- crates/librqbit/src/torrent_state/live/mod.rs | 28 ++++- crates/librqbit/src/torrent_state/mod.rs | 116 +++++++++++++----- 2 files changed, 109 insertions(+), 35 deletions(-) diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 4045d85..5aba636 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -137,6 +137,9 @@ pub(crate) struct TorrentStateLocked { // At a moment in time, we are expecting a piece from only one peer. // inflight_pieces stores this information. inflight_pieces: HashMap, + + // If this is None, then it was already used + fatal_errors_tx: Option>, } impl TorrentStateLocked { @@ -187,7 +190,10 @@ pub struct TorrentStateLive { } impl TorrentStateLive { - pub(crate) fn new(paused: TorrentStatePaused) -> Arc { + pub(crate) fn new( + paused: TorrentStatePaused, + fatal_errors_tx: tokio::sync::oneshot::Sender, + ) -> Arc { let (peer_queue_tx, peer_queue_rx) = unbounded_channel(); let speed_estimator = SpeedEstimator::new(5); @@ -204,6 +210,7 @@ impl TorrentStateLive { locked: RwLock::new(TorrentStateLocked { chunks: Some(paused.chunk_tracker), inflight_pieces: Default::default(), + fatal_errors_tx: Some(fatal_errors_tx), }), files: paused.files, filenames: paused.filenames, @@ -438,6 +445,10 @@ impl TorrentStateLive { } } + pub fn meta(&self) -> &ManagedTorrentInfo { + &self.meta + } + pub fn info(&self) -> &TorrentMetaV1Info { &self.meta.info } @@ -668,6 +679,19 @@ impl TorrentStateLive { have_bytes, }) } + + fn on_fatal_error(&self, e: anyhow::Error) -> anyhow::Result<()> { + let mut g = self.lock_write("fatal_error"); + let tx = g + .fatal_errors_tx + .take() + .context("fatal_errors_tx already taken")?; + let res = anyhow::anyhow!("fatal error: {:?}", e); + if tx.send(e).is_err() { + warn!("there's nowhere to send fatal error, receiver is dead"); + } + Err(res) + } } struct PeerHandlerLocked { @@ -1286,7 +1310,7 @@ impl PeerHandler { Ok(()) => {} Err(e) => { error!("FATAL: error writing chunk to disk: {:?}", e); - panic!("{:?}", e); + return self.state.on_fatal_error(e); } } diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 2cc9fcd..31b9ca5 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -10,7 +10,6 @@ use std::path::Path; use std::path::PathBuf; use std::sync::atomic::Ordering; use std::sync::Arc; -use std::sync::Weak; use std::time::Duration; use anyhow::bail; @@ -28,6 +27,7 @@ use tokio_stream::StreamExt; use tracing::debug; use tracing::error; use tracing::error_span; +use tracing::warn; use url::Url; use crate::chunk_tracker::ChunkTracker; @@ -138,6 +138,30 @@ impl ManagedTorrent { } } + fn stop_with_error(&self, error: anyhow::Error) { + let mut g = self.locked.write(); + + match g.state.take() { + ManagedTorrentState::Live(live) => { + if let Err(err) = live.pause() { + warn!( + "error pausing live torrent during fatal error handling: {:?}", + err + ); + } + } + ManagedTorrentState::Error(e) => { + warn!("bug: torrent already was in error state when trying to stop it. Previous error was: {:?}", e); + } + ManagedTorrentState::None => { + warn!("bug: torrent encountered in None state during fatal error handling") + } + _ => {} + }; + + g.state = ManagedTorrentState::Error(error) + } + pub fn start( self: &Arc, initial_peers: Vec, @@ -146,30 +170,59 @@ impl ManagedTorrent { ) -> anyhow::Result<()> { let mut g = self.locked.write(); - let peer_adder = |live: Weak| async move { - { - let live: Arc = live.upgrade().context("no longer live")?; - for peer in initial_peers { - live.add_peer_if_not_seen(peer).context("torrent closed")?; - } - } + let spawn_fatal_errors_receiver = + |state: &Arc, rx: tokio::sync::oneshot::Receiver| { + let span = state.info.span.clone(); + let state = Arc::downgrade(state); + spawn( + "fatal_errors_receiver", + error_span!(parent: span, "fatal_errors_receiver"), + async move { + let e = match rx.await { + Ok(e) => e, + Err(_) => return Ok(()), + }; + if let Some(state) = state.upgrade() { + state.stop_with_error(e); + } else { + warn!("tried to stop the torrent with error, but it's couldn't upgrade the arc"); + } + Ok(()) + }, + ); + }; - if let Some(mut peer_rx) = peer_rx { - while let Some(peer) = peer_rx.next().await { - live.upgrade() - .context("no longer live")? - .add_peer_if_not_seen(peer) - .context("torrent closed")?; - } - } else { - error!("peer rx is not set"); - } + let spawn_peer_adder = |live: &Arc| { + let span = live.meta().span.clone(); + let live = Arc::downgrade(live); + spawn( + "external_peer_adder", + error_span!(parent: span, "external_peer_adder"), + async move { + { + let live: Arc = + live.upgrade().context("no longer live")?; + for peer in initial_peers { + live.add_peer_if_not_seen(peer).context("torrent closed")?; + } + } - Ok(()) + if let Some(mut peer_rx) = peer_rx { + while let Some(peer) = peer_rx.next().await { + live.upgrade() + .context("no longer live")? + .add_peer_if_not_seen(peer) + .context("torrent closed")?; + } + } else { + error!("peer rx is not set"); + } + + Ok(()) + }, + ); }; - let span = self.info.span.clone(); - match &g.state { ManagedTorrentState::Live(_) => { bail!("torrent is already live"); @@ -177,6 +230,7 @@ impl ManagedTorrent { ManagedTorrentState::Initializing(init) => { let init = init.clone(); let t = self.clone(); + let span = self.info().span.clone(); spawn( "initialize_and_start", error_span!(parent: span.clone(), "initialize_and_start"), @@ -195,14 +249,12 @@ impl ManagedTorrent { return Ok(()); } - let live = TorrentStateLive::new(paused); + let (tx, rx) = tokio::sync::oneshot::channel(); + let live = TorrentStateLive::new(paused, tx); g.state = ManagedTorrentState::Live(live.clone()); - spawn( - "external_peer_adder", - error_span!(parent: span.clone(), "external_peer_adder"), - peer_adder(Arc::downgrade(&live)), - ); + spawn_fatal_errors_receiver(&t, rx); + spawn_peer_adder(&live); Ok(()) } @@ -218,13 +270,11 @@ impl ManagedTorrent { } ManagedTorrentState::Paused(_) => { let paused = g.state.take().assert_paused(); - let live = TorrentStateLive::new(paused); + let (tx, rx) = tokio::sync::oneshot::channel(); + let live = TorrentStateLive::new(paused, tx); g.state = ManagedTorrentState::Live(live.clone()); - spawn( - "external_peer_adder", - error_span!(parent: span.clone(), "external_peer_adder"), - peer_adder(Arc::downgrade(&live)), - ); + spawn_fatal_errors_receiver(self, rx); + spawn_peer_adder(&live); Ok(()) } ManagedTorrentState::Error(_) => { From d8fdb94305344684ed6105f56f6db475bb8a07cb Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 15:15:16 +0000 Subject: [PATCH 31/38] DHT instrumentation --- Cargo.lock | 12 +++--- crates/dht/Cargo.toml | 4 +- crates/dht/src/dht.rs | 53 ++++++++++++++----------- crates/dht/src/persistence.rs | 6 +-- crates/librqbit/Cargo.toml | 8 ++-- crates/librqbit/src/spawn_utils.rs | 16 +------- crates/librqbit_core/Cargo.toml | 4 +- crates/librqbit_core/src/lib.rs | 1 + crates/librqbit_core/src/spawn_utils.rs | 20 ++++++++++ crates/peer_binary_protocol/Cargo.toml | 4 +- crates/rqbit/Cargo.toml | 6 +-- 11 files changed, 76 insertions(+), 58 deletions(-) create mode 100644 crates/librqbit_core/src/spawn_utils.rs diff --git a/Cargo.lock b/Cargo.lock index 60c88b0..b7dec58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1000,7 +1000,7 @@ dependencies = [ [[package]] name = "librqbit" -version = "3.3.0" +version = "4.0.0-beta.0" dependencies = [ "anyhow", "axum", @@ -1066,7 +1066,7 @@ version = "2.2.1" [[package]] name = "librqbit-core" -version = "3.0.0" +version = "3.1.0" dependencies = [ "anyhow", "hex 0.4.3", @@ -1076,13 +1076,15 @@ dependencies = [ "librqbit-clone-to-owned", "parking_lot", "serde", + "tokio", + "tracing", "url", "uuid", ] [[package]] name = "librqbit-dht" -version = "3.1.0" +version = "3.2.0" dependencies = [ "anyhow", "directories", @@ -1105,7 +1107,7 @@ dependencies = [ [[package]] name = "librqbit-peer-protocol" -version = "3.0.0" +version = "3.1.0" dependencies = [ "anyhow", "bincode", @@ -1704,7 +1706,7 @@ dependencies = [ [[package]] name = "rqbit" -version = "3.3.0" +version = "4.0.0-beta.0" dependencies = [ "anyhow", "clap", diff --git a/crates/dht/Cargo.toml b/crates/dht/Cargo.toml index fa69e28..ec5c9d5 100644 --- a/crates/dht/Cargo.toml +++ b/crates/dht/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit-dht" -version = "3.1.0" +version = "3.2.0" edition = "2021" description = "DHT implementation, used in rqbit torrent client." license = "Apache-2.0" @@ -33,7 +33,7 @@ indexmap = "2" directories = "5" clone_to_owned = {path="../clone_to_owned", package="librqbit-clone-to-owned", version = "2.2.1"} -librqbit-core = {path="../librqbit_core", version = "3.0.0"} +librqbit-core = {path="../librqbit_core", version = "3.1.0"} [dev-dependencies] tracing-subscriber = "0.3" \ No newline at end of file diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index bcbc576..f2331e2 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -18,7 +18,7 @@ use bencode::ByteString; use futures::{stream::FuturesUnordered, Stream, StreamExt}; use indexmap::IndexSet; use leaky_bucket::RateLimiter; -use librqbit_core::{id20::Id20, peer_id::generate_peer_id}; +use librqbit_core::{id20::Id20, peer_id::generate_peer_id, spawn_utils::spawn}; use parking_lot::RwLock; use rand::Rng; use serde::Serialize; @@ -27,7 +27,7 @@ use tokio::{ sync::mpsc::{channel, unbounded_channel, Sender, UnboundedReceiver, UnboundedSender}, }; use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, debug_span, error_span, info, trace, warn, Instrument}; #[derive(Debug, Serialize)] pub struct DhtStats { @@ -513,7 +513,7 @@ impl DhtWorker { bootstrap_addrs: &[String], ) -> anyhow::Result<()> { let (out_tx, mut out_rx) = channel(1); - let framer = run_framer(&self.socket, in_rx, out_tx); + let framer = run_framer(&self.socket, in_rx, out_tx).instrument(debug_span!("dht_framer")); let bootstrap = async { let mut futs = FuturesUnordered::new(); @@ -521,34 +521,40 @@ impl DhtWorker { for addr in bootstrap_addrs.iter() { let this = &self; let in_tx = &in_tx; - futs.push(async move { - match tokio::net::lookup_host(addr).await { - Ok(addrs) => { - for addr in addrs { - let request = this - .state - .write() - .create_request(Request::FindNode(this.peer_id), addr); - in_tx.send((request, addr))?; + futs.push( + async move { + match tokio::net::lookup_host(addr).await { + Ok(addrs) => { + for addr in addrs { + let request = this + .state + .write() + .create_request(Request::FindNode(this.peer_id), addr); + in_tx.send((request, addr))?; + } + } + Err(e) => { + warn!("error looking up {}: {}", addr, e); + return Err(e.into()); } } - Err(e) => warn!("error looking up {}: {}", addr, e), + Ok::<_, anyhow::Error>(()) } - Ok::<_, anyhow::Error>(()) - }); + .instrument(error_span!("dht_bootstrap", addr = addr)), + ); } let mut successes = 0; while let Some(resp) = futs.next().await { - match resp { - Ok(_) => successes += 1, - Err(e) => warn!("error in one of the bootstrappers: {}", e), + if resp.is_ok() { + successes += 1 } } if successes == 0 { anyhow::bail!("bootstrapping did not succeed") } Ok(()) - }; + } + .instrument(debug_span!("dht_bootstrapper")); let mut bootstrap_done = false; let response_reader = { @@ -563,7 +569,8 @@ impl DhtWorker { "closed response reader, nowhere to send results to, DHT closed" )) } - }; + } + .instrument(debug_span!("dht_responese_reader")); tokio::pin!(framer); tokio::pin!(bootstrap); @@ -676,7 +683,7 @@ impl Dht { listen_addr, ))); - tokio::spawn({ + spawn(error_span!("dht"), { let state = state.clone(); async move { let worker = DhtWorker { @@ -684,8 +691,8 @@ impl Dht { peer_id, state, }; - let result = worker.start(in_tx, in_rx, &bootstrap_addrs).await; - warn!("DHT worker finished with {:?}", result); + worker.start(in_tx, in_rx, &bootstrap_addrs).await?; + Ok(()) } }); Ok(Dht { state }) diff --git a/crates/dht/src/persistence.rs b/crates/dht/src/persistence.rs index d22ad6b..a4f091e 100644 --- a/crates/dht/src/persistence.rs +++ b/crates/dht/src/persistence.rs @@ -1,5 +1,6 @@ // TODO: this now stores only the routing table, but we also need AT LEAST the same socket address... +use librqbit_core::spawn_utils::spawn; use serde::{Deserialize, Serialize}; use std::fs::OpenOptions; use std::io::{BufReader, BufWriter}; @@ -8,8 +9,7 @@ use std::path::{Path, PathBuf}; use std::time::Duration; use anyhow::Context; -use tokio::spawn; -use tracing::{debug, error, info, trace, warn}; +use tracing::{debug, error, error_span, info, trace, warn}; use crate::dht::{Dht, DhtConfig}; use crate::routing_table::RoutingTable; @@ -110,7 +110,7 @@ impl PersistentDht { }; let dht = Dht::with_config(dht_config).await?; - spawn({ + spawn(error_span!("dht_persistence"), { let dht = dht.clone(); let dump_interval = config .dump_interval diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index 01beab9..1104017 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit" -version = "3.3.0" +version = "4.0.0-beta.0" authors = ["Igor Katson "] edition = "2021" description = "The main library used by rqbit torrent client. The binary is just a small wrapper on top of it." @@ -24,11 +24,11 @@ rust-tls = ["reqwest/rustls-tls"] [dependencies] bencode = {path = "../bencode", default-features=false, package="librqbit-bencode", version="2.2.1"} buffers = {path = "../buffers", package="librqbit-buffers", version = "2.2.1"} -librqbit-core = {path = "../librqbit_core", version = "3.0.0"} +librqbit-core = {path = "../librqbit_core", version = "3.1.0"} clone_to_owned = {path = "../clone_to_owned", package="librqbit-clone-to-owned", version = "2.2.1"} -peer_binary_protocol = {path = "../peer_binary_protocol", package="librqbit-peer-protocol", version = "3.0.0"} +peer_binary_protocol = {path = "../peer_binary_protocol", package="librqbit-peer-protocol", version = "3.1.0"} sha1w = {path = "../sha1w", default-features=false, package="librqbit-sha1-wrapper", version="2.2.1"} -dht = {path = "../dht", package="librqbit-dht", version="3.1.0"} +dht = {path = "../dht", package="librqbit-dht", version="3.2.0"} tokio = {version = "1", features = ["macros", "rt-multi-thread"]} axum = {version = "0.6"} diff --git a/crates/librqbit/src/spawn_utils.rs b/crates/librqbit/src/spawn_utils.rs index 1e404bd..3cb4aff 100644 --- a/crates/librqbit/src/spawn_utils.rs +++ b/crates/librqbit/src/spawn_utils.rs @@ -1,23 +1,9 @@ -use tracing::{debug, trace, warn, Instrument}; - pub fn spawn( _name: &str, span: tracing::Span, fut: impl std::future::Future> + Send + 'static, ) -> tokio::task::JoinHandle<()> { - let fut = async move { - trace!("started"); - match fut.await { - Ok(_) => { - debug!("finished"); - } - Err(e) => { - warn!("finished with error: {:#}", e) - } - } - } - .instrument(span.or_current()); - tokio::task::spawn(fut) + librqbit_core::spawn_utils::spawn(span, fut) } #[derive(Clone, Copy, Debug)] diff --git a/crates/librqbit_core/Cargo.toml b/crates/librqbit_core/Cargo.toml index 73ac147..4d1df54 100644 --- a/crates/librqbit_core/Cargo.toml +++ b/crates/librqbit_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit-core" -version = "3.0.0" +version = "3.1.0" edition = "2021" description = "Important utilities used throughout librqbit useful for working with torrents." license = "Apache-2.0" @@ -17,6 +17,8 @@ sha1-openssl = ["bencode/sha1-openssl"] sha1-rust = ["bencode/sha1-rust"] [dependencies] +tracing = "0.1.40" +tokio = "1" hex = "0.4" anyhow = "1" url = "2" diff --git a/crates/librqbit_core/src/lib.rs b/crates/librqbit_core/src/lib.rs index 1c4d034..16e42d3 100644 --- a/crates/librqbit_core/src/lib.rs +++ b/crates/librqbit_core/src/lib.rs @@ -3,5 +3,6 @@ pub mod id20; pub mod lengths; pub mod magnet; pub mod peer_id; +pub mod spawn_utils; pub mod speed_estimator; pub mod torrent_metainfo; diff --git a/crates/librqbit_core/src/spawn_utils.rs b/crates/librqbit_core/src/spawn_utils.rs new file mode 100644 index 0000000..81e9b00 --- /dev/null +++ b/crates/librqbit_core/src/spawn_utils.rs @@ -0,0 +1,20 @@ +use tracing::{debug, error, trace, Instrument}; + +pub fn spawn( + span: tracing::Span, + fut: impl std::future::Future> + Send + 'static, +) -> tokio::task::JoinHandle<()> { + let fut = async move { + trace!("started"); + match fut.await { + Ok(_) => { + debug!("finished"); + } + Err(e) => { + error!("finished with error: {:#}", e) + } + } + } + .instrument(span); + tokio::task::spawn(fut) +} diff --git a/crates/peer_binary_protocol/Cargo.toml b/crates/peer_binary_protocol/Cargo.toml index aa0df2e..6da24f1 100644 --- a/crates/peer_binary_protocol/Cargo.toml +++ b/crates/peer_binary_protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit-peer-protocol" -version = "3.0.0" +version = "3.1.0" edition = "2021" description = "Protocol for working with torrent peers. Used in rqbit torrent client." license = "Apache-2.0" @@ -23,6 +23,6 @@ byteorder = "1" buffers = {path="../buffers", package="librqbit-buffers", version = "2.2.1"} bencode = {path = "../bencode", default-features=false, package="librqbit-bencode", version="2.2.1"} clone_to_owned = {path="../clone_to_owned", package="librqbit-clone-to-owned", version = "2.2.1"} -librqbit-core = {path="../librqbit_core", version = "3.0.0"} +librqbit-core = {path="../librqbit_core", version = "3.1.0"} bitvec = "1" anyhow = "1" \ No newline at end of file diff --git a/crates/rqbit/Cargo.toml b/crates/rqbit/Cargo.toml index e2a1d97..f879940 100644 --- a/crates/rqbit/Cargo.toml +++ b/crates/rqbit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rqbit" -version = "3.3.0" +version = "4.0.0-beta.0" authors = ["Igor Katson "] edition = "2021" description = "A bittorrent command line client and server." @@ -23,8 +23,8 @@ default-tls = ["librqbit/default-tls"] rust-tls = ["librqbit/rust-tls"] [dependencies] -librqbit = {path="../librqbit", default-features=false, version = "3.3.0"} -dht = {path="../dht", package="librqbit-dht", version="3.1.0"} +librqbit = {path="../librqbit", default-features=false, version = "4.0.0-beta.0"} +dht = {path="../dht", package="librqbit-dht", version="3.2.0"} tokio = {version = "1", features = ["macros", "rt-multi-thread"]} console-subscriber = {version = "0.2", optional = true} anyhow = "1" From e3a3f71064b3dcf15766a319dbca49db80b3b68c Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 17:13:17 +0000 Subject: [PATCH 32/38] Nothing --- crates/librqbit/src/session.rs | 8 +++++++- crates/librqbit/src/torrent_state/initializing.rs | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index b8531a9..2355518 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -363,6 +363,9 @@ impl Session { opts: Option, ) -> anyhow::Result { // Magnet links are different in that we first need to discover the metadata. + let span = error_span!("add_torrent"); + let _ = span.enter(); + let opts = opts.unwrap_or_default(); let (info_hash, info, dht_rx, trackers, initial_peers) = match add.into() { @@ -389,6 +392,7 @@ impl Session { }) .collect(); + debug!("querying DHT for {:?}", info_hash); let (info, dht_rx, initial_peers) = match read_metainfo_from_peer_receiver( self.peer_id, info_hash, @@ -402,6 +406,7 @@ impl Session { anyhow::bail!("DHT died, no way to discover torrent metainfo") } }; + debug!("received result from DHT: {:?}", info); (info_hash, info, Some(dht_rx), trackers, initial_peers) } other => { @@ -558,7 +563,8 @@ impl Session { return Ok(AddTorrentResponse::AlreadyManaged(*id, handle.clone())); } let next_id = g.torrents.len(); - let managed_torrent = builder.build(error_span!("torrent", id = next_id))?; + let managed_torrent = + builder.build(error_span!(parent: None, "torrent", id = next_id))?; let id = g.add_torrent(managed_torrent.clone()); (managed_torrent, id) }; diff --git a/crates/librqbit/src/torrent_state/initializing.rs b/crates/librqbit/src/torrent_state/initializing.rs index d9d2ae0..45d7ada 100644 --- a/crates/librqbit/src/torrent_state/initializing.rs +++ b/crates/librqbit/src/torrent_state/initializing.rs @@ -58,7 +58,10 @@ impl TorrentStateInitializing { .create(true) .read(true) .write(true) - .open(&full_path)? + .open(&full_path) + .with_context(|| { + format!("error opening {full_path:?} in read/write mode") + })? } else { // TODO: create_new does not seem to work with read(true), so calling this twice. OpenOptions::new() From 3de67d09024cc57fbc0cebdd862fccd1a17d0f81 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 18:42:43 +0000 Subject: [PATCH 33/38] Can now try to unpause errored torrent --- TODO.md | 14 +++++++---- crates/librqbit/src/torrent_state/mod.rs | 31 +++++++++++++++++------- crates/librqbit/webui/dist/app.js | 2 +- crates/librqbit/webui/src/index.tsx | 2 +- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/TODO.md b/TODO.md index 118a23c..987dd0d 100644 --- a/TODO.md +++ b/TODO.md @@ -14,16 +14,20 @@ - [x] pause/unpause - [x] remove including from disk - [ ] DHT - - [ ] for torrents with a few seeds might be cool to re-query DHT once in a while + - [ ] for torrents with a few seeds might be cool to re-query DHT once in a while. - [x] it's sending many requests now way too fast, locks up Mac OS UI annoyingly someday: -- [ ] cancellation from the client-side for the lib (i.e. stop the torrent manager) +- [x] cancellation from the client-side for the lib (i.e. stop the torrent manager) refactor: - [x] where are peers stored - [x] http api pause/unpause etc -- [ ] when a live torrent fails writing to disk, it should transition to error state -- [ ] something is wrong when unpausing - can't finish. Recalculate needed/have from chunk tracker. -- [ ] silence this: WARN torrent{id=0}:external_peer_adder: librqbit::spawn_utils: finished with error: no longer live +- [x] when a live torrent fails writing to disk, it should transition to error state +- [x] something is wrong when unpausing - can't finish. Recalculate needed/have from chunk tracker. +- [x] silence this: WARN torrent{id=0}:external_peer_adder: librqbit::spawn_utils: finished with error: no longer live + +- [x] start from error state should be possible from UI +- [ ] if the torrent was completed, not need to re-check it +- [ ] checking is very slow on raspberry \ No newline at end of file diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 31b9ca5..cd9f01d 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -192,7 +192,11 @@ impl ManagedTorrent { ); }; - let spawn_peer_adder = |live: &Arc| { + fn spawn_peer_adder( + live: &Arc, + initial_peers: Vec, + peer_rx: Option + Unpin + Send + Sync + 'static>, + ) { let span = live.meta().span.clone(); let live = Arc::downgrade(live); spawn( @@ -209,10 +213,11 @@ impl ManagedTorrent { if let Some(mut peer_rx) = peer_rx { while let Some(peer) = peer_rx.next().await { - live.upgrade() - .context("no longer live")? - .add_peer_if_not_seen(peer) - .context("torrent closed")?; + let live = match live.upgrade() { + Some(live) => live, + None => return Ok(()), + }; + live.add_peer_if_not_seen(peer).context("torrent closed")?; } } else { error!("peer rx is not set"); @@ -221,7 +226,7 @@ impl ManagedTorrent { Ok(()) }, ); - }; + } match &g.state { ManagedTorrentState::Live(_) => { @@ -254,7 +259,7 @@ impl ManagedTorrent { g.state = ManagedTorrentState::Live(live.clone()); spawn_fatal_errors_receiver(&t, rx); - spawn_peer_adder(&live); + spawn_peer_adder(&live, initial_peers, peer_rx); Ok(()) } @@ -274,11 +279,19 @@ impl ManagedTorrent { let live = TorrentStateLive::new(paused, tx); g.state = ManagedTorrentState::Live(live.clone()); spawn_fatal_errors_receiver(self, rx); - spawn_peer_adder(&live); + spawn_peer_adder(&live, initial_peers, peer_rx); Ok(()) } ManagedTorrentState::Error(_) => { - bail!("starting torrents from error state not implemented") + let initializing = Arc::new(TorrentStateInitializing::new( + self.info.clone(), + self.only_files.clone(), + )); + g.state = ManagedTorrentState::Initializing(initializing.clone()); + drop(g); + + // Recurse. + self.start(initial_peers, peer_rx, start_paused) } ManagedTorrentState::None => bail!("bug: torrent is in empty state"), } diff --git a/crates/librqbit/webui/dist/app.js b/crates/librqbit/webui/dist/app.js index 765ee33..4330772 100644 --- a/crates/librqbit/webui/dist/app.js +++ b/crates/librqbit/webui/dist/app.js @@ -41,4 +41,4 @@ Error generating stack: `+o.message+` Copyright (c) 2018 Jed Watson. Licensed under the MIT License (MIT), see http://jedwatson.github.io/classnames -*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function mh(e){var t=hh(e,"string");return typeof t=="symbol"?t:String(t)}function hh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function vh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var d=arguments.length,m=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Bh(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Uh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function mo(e,t){return y.useMemo(()=>Uh(e,t),[e,t])}function Hh(e){return e&&"setState"in e?Cn.findDOMNode(e):e??null}const Wh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const m=y.useRef(null),p=mo(m,s),g=N=>{p(Hh(N))},w=N=>T=>{N&&m.current&&N(m.current,T)},k=y.useCallback(w(e),[e]),R=y.useCallback(w(t),[t]),f=y.useCallback(w(n),[n]),c=y.useCallback(w(r),[r]),h=y.useCallback(w(l),[l]),S=y.useCallback(w(o),[o]),C=y.useCallback(w(i),[i]);return v.jsx(zh,{ref:d,...a,onEnter:k,onEntered:f,onEntering:R,onExit:c,onExited:S,onExiting:h,addEndListener:C,nodeRef:m,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Wt.cloneElement(u,{ref:g})})}),Vh=Wh;function Qh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Qh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Qf=e=>y.forwardRef((t,n)=>v.jsx("div",{...t,ref:n,className:M(t.className,e)})),Kf=Qf("h4");Kf.displayName="DivStyledAsH4";const Gf=y.forwardRef(({className:e,bsPrefix:t,as:n=Kf,...r},l)=>(t=H(t,"alert-heading"),v.jsx(n,{ref:l,className:M(e,t),...r})));Gf.displayName="AlertHeading";const Kh=Gf;function Gh(){return y.useState(null)}function Yh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Xh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Zh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Jh=typeof document<"u",ca=Jh||Zh?y.useLayoutEffect:y.useEffect,qh=["as","disabled"];function bh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function ev(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&ev(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},m=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:m},a]}const tv=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=bh(e,qh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return v.jsx(i,Object.assign({},l,o,{ref:t}))});tv.displayName="Button";const nv=["onKeyDown"];function rv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function lv(e){return!e||e.trim()==="#"}const Yf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=rv(e,nv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return lv(r.href)||r.role==="button"?v.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):v.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Yf.displayName="Anchor";const ov=Yf,Xf=y.forwardRef(({className:e,bsPrefix:t,as:n=ov,...r},l)=>(t=H(t,"alert-link"),v.jsx(n,{ref:l,className:M(e,t),...r})));Xf.displayName="AlertLink";const iv=Xf,uv={[St]:"show",[Ht]:"show"},Zf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Bh(s),r==null||r(s,a)},[r]);return v.jsx(Vh,{ref:o,addEndListener:Ah,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,uv[s],n[s])})})});Zf.displayName="Fade";const Kl=Zf,sv={"aria-label":ot.string,onClick:ot.func,variant:ot.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>v.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=sv;const Jf=Wu,qf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:m=Kl,...p}=yh(e,{show:"onClose"}),g=H(n,"alert"),w=Pe(f=>{a&&a(!1,f)}),k=m===!0?Kl:m,R=v.jsxs("div",{role:"alert",...k?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&v.jsx(Jf,{onClick:w,"aria-label":l,variant:o}),u]});return k?v.jsx(k,{unmountOnExit:!0,...p,ref:void 0,in:r,children:R}):r?R:null});qf.displayName="Alert";const fa=Object.assign(qf,{Link:iv,Heading:Kh}),bf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:m}]=Hu({tagName:e,disabled:o,...u}),p=m;return v.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});bf.displayName="Button";const Mr=bf;function av(e){const t=y.useRef(e);return t.current=e,t}function ed(e){const t=av(e);y.useEffect(()=>()=>t.current(),[])}function cv(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function fv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function dv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=Df(),o=If(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,m,p;typeof a=="object"&&a!=null?{span:d,offset:m,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),m!=null&&u.push(`offset${g}-${m}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const td=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=dv(e);return v.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});td.displayName="Col";const Vu=td,nd=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return v.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});nd.displayName="Container";const pv=nd;var mv=Function.prototype.bind.call(Function.prototype.call,[].slice);function cn(e,t){return mv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const hv="data-rr-ui-";function vv(e){return`${hv}${e}`}const rd=y.createContext(Wn?window:void 0);rd.Provider;function Qu(){return y.useContext(rd)}const yv={type:ot.string,tooltip:ot.bool,as:ot.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>v.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=yv;const ld=Ku,gv=y.createContext({}),ct=gv,od=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ct);return t=H(t,"form-check-input"),v.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});od.displayName="FormCheckInput";const id=od,ud=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-check-label"),v.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});ud.displayName="FormCheckLabel";const Gi=ud,sd=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:m,style:p,title:g="",type:w="checkbox",label:k,children:R,as:f="input",...c},h)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ct),C=y.useMemo(()=>({controlId:e||S}),[S,e]),N=!R&&k!=null&&k!==!1||fv(R,Gi),T=v.jsx(id,{...c,type:w==="switch"?"checkbox":w,ref:h,isValid:i,isInvalid:u,disabled:o,as:f});return v.jsx(ct.Provider,{value:C,children:v.jsx("div",{style:p,className:M(m,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,w==="switch"&&n),children:R||v.jsxs(v.Fragment,{children:[T,N&&v.jsx(Gi,{title:g,children:k}),a&&v.jsx(ld,{type:d,tooltip:s,children:a})]})})})});sd.displayName="FormCheck";const Gl=Object.assign(sd,{Input:id,Label:Gi}),ad=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...m},p)=>{const{controlId:g}=y.useContext(ct);return e=H(e,"form-control"),v.jsx(d,{...m,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});ad.displayName="FormControl";const wv=Object.assign(ad,{Feedback:ld}),cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),v.jsx(n,{ref:l,className:M(e,t),...r})));cd.displayName="FormFloating";const Sv=cd,fd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return v.jsx(ct.Provider,{value:l,children:v.jsx(t,{...n,ref:r})})});fd.displayName="FormGroup";const dd=fd,pd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ct);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?v.jsx(Vu,{ref:u,as:"label",className:d,htmlFor:o,...i}):v.jsx(e,{ref:u,className:d,htmlFor:o,...i})});pd.displayName="FormLabel";const kv=pd,md=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-range"),v.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});md.displayName="FormRange";const Ev=md,hd=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ct);return e=H(e,"form-select"),v.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});hd.displayName="FormSelect";const xv=hd,vd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),v.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));vd.displayName="FormText";const Cv=vd,yd=y.forwardRef((e,t)=>v.jsx(Gl,{...e,ref:t,type:"switch"}));yd.displayName="Switch";const Nv=Object.assign(yd,{Input:Gl.Input,Label:Gl.Label}),gd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),v.jsxs(dd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,v.jsx("label",{htmlFor:r,children:l})]})));gd.displayName="FloatingLabel";const Tv=gd,_v={_ref:ot.any,validated:ot.bool,as:ot.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>v.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=_v;const On=Object.assign(Gu,{Group:dd,Control:wv,Floating:Sv,Check:Gl,Switch:Nv,Label:kv,Text:Cv,Range:Ev,Select:xv,FloatingLabel:Tv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Wn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Wo(e){e===void 0&&(e=po());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function jv(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=vv("modal-open");class Rv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return jv(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=Rv,Vo=(e,t)=>Wn?e==null?(t||po()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Lv(e,t){const n=Qu(),[r,l]=y.useState(()=>Vo(e,n==null?void 0:n.document));if(!r){const o=Vo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Vo(e);o!==r&&l(o)},[e,r]),r}function Ov({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Pe(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=mo(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Pv({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Pe(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Fv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Pv({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,m=>{throw a.in||i(!0),m})}}),s=mo(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?v.jsx(e,Object.assign({},n)):t?v.jsx(Fv,Object.assign({},n,{transition:t})):v.jsx(Ov,Object.assign({},n))}function Mv(e){return e.code==="Escape"||e.keyCode===27}const zv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function $v(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Qo;function Dv(e){return Qo||(Qo=new Yu({ownerDocument:e==null?void 0:e.document})),Qo}function Iv(e){const t=Qu(),n=e||Dv(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const wd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:m,runTransition:p,backdropTransition:g,runBackdropTransition:w,autoFocus:k=!0,enforceFocus:R=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:h,renderBackdrop:S=K=>v.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:j=()=>{},onExit:B,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:rn}=e,Qn=$v(e,zv);const _e=Qu(),Ke=Lv(N),x=Iv(C),L=Yh(),O=Xh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>x,[x]),Wn&&!O&&n&&(fe.current=Wo(_e==null?void 0:_e.document)),n&&D&&A(!1);const je=Pe(()=>{if(x.add(),on.current=Ql(document,"keydown",ho),ln.current=Ql(document,"focus",()=>setTimeout(Re),!0),T&&T(),k){var K,Hr;const Yn=Wo((K=(Hr=x.dialog)==null?void 0:Hr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Yn&&!da(x.dialog,Yn)&&(fe.current=Yn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),on.current==null||on.current(),ln.current==null||ln.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ke||je()},[n,Ke,je]),y.useEffect(()=>{D&&qe()},[D,qe]),ed(()=>{qe()});const Re=Pe(()=>{if(!R||!L()||!x.isTopModal())return;const K=Wo(_e==null?void 0:_e.document);x.dialog&&K&&!da(x.dialog,K)&&x.dialog.focus()}),mt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),ho=Pe(K=>{s&&Mv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||j())}),ln=y.useRef(),on=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const Ur=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=h?h(Ur):v.jsx("div",Object.assign({},Ur,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(m,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:B,onExiting:ie,onExited:Kn,onEnter:Ve,onEntering:Qe,onEntered:rn,children:Gn});let It=null;return u&&(It=S({ref:x.setBackdropRef,onClick:mt}),It=ha(g,w,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),v.jsx(v.Fragment,{children:Cn.createPortal(v.jsxs(v.Fragment,{children:[It,Gn]}),Ke)})});wd.displayName="Modal";const Av=Object.assign(wd,{Manager:Yu});function Bv(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Uv(e,t){e.classList?e.classList.add(t):Bv(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Hv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const fn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Wv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Uv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Hv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Ko;function Vv(e){return Ko||(Ko=new Wv(e)),Ko}const Sd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),v.jsx(n,{ref:l,className:M(e,t),...r})));Sd.displayName="ModalBody";const Qv=Sd,Kv=y.createContext({onHide(){}}),kd=Kv,Ed=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,m=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return v.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&m),children:v.jsx("div",{className:M(`${e}-content`,n),children:i})})});Ed.displayName="ModalDialog";const xd=Ed,Cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),v.jsx(n,{ref:l,className:M(e,t),...r})));Cd.displayName="ModalFooter";const Gv=Cd,Yv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(kd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return v.jsxs("div",{ref:i,...o,children:[l,n&&v.jsx(Jf,{"aria-label":e,variant:t,onClick:s})]})}),Xv=Yv,Nd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),v.jsx(Xv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Nd.displayName="ModalHeader";const Zv=Nd,Jv=Qf("h4"),Td=y.forwardRef(({className:e,bsPrefix:t,as:n=Jv,...r},l)=>(t=H(t,"modal-title"),v.jsx(n,{ref:l,className:M(e,t),...r})));Td.displayName="ModalTitle";const qv=Td;function bv(e){return v.jsx(Kl,{...e,timeout:null})}function ey(e){return v.jsx(Kl,{...e,timeout:null})}const _d=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=xd,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:m=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:w,onShow:k,onHide:R,container:f,autoFocus:c=!0,enforceFocus:h=!0,restoreFocus:S=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:j,onEnter:B,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...rn},Qn)=>{const[_e,Ke]=y.useState({}),[x,L]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,je]=Gh(),qe=mo(Qn,je),Re=Pe(R),mt=kh();e=H(e,"modal");const ho=y.useMemo(()=>({onHide:Re}),[Re]);function ln(){return Qe||Vv({isRTL:mt})}function on($){if(!Wn)return;const un=ln().getScrollbarWidth()>0,Zu=$.scrollHeight>po($).documentElement.clientHeight;Ke({paddingRight:un&&!Zu?pa():void 0,paddingLeft:!un&&Zu?pa():void 0})}const Kn=Pe(()=>{fe&&on(fe.dialog)});ed(()=>{Ki(window,"resize",Kn),A.current==null||A.current()});const Ur=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{L(!0),A.current=Vf(fe.dialog,()=>{L(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}R==null||R()},Yn=$=>{g?w==null||w($):($.preventDefault(),p==="static"&&It())},Dd=($,un)=>{$&&on($),B==null||B($,un)},Id=$=>{A.current==null||A.current(),T==null||T($)},Ad=($,un)=>{P==null||P($,un),Wf(window,"resize",Kn)},Bd=$=>{$&&($.style.display=""),ie==null||ie($),Ki(window,"resize",Kn)},Ud=y.useCallback($=>v.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!m&&"show")}),[m,Ve,e]),Xu={...n,..._e};Xu.display="block";const Hd=$=>v.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,x&&`${e}-static`,!m&&"show"),onClick:p?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:v.jsx(i,{...rn,onMouseDown:Ur,className:r,contentClassName:l,children:o})});return v.jsx(kd.Provider,{value:ho,children:v.jsx(Av,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:h,restoreFocus:S,restoreFocusOptions:C,onEscapeKeyDown:Yn,onShow:k,onHide:R,onEnter:Dd,onEntering:Ad,onEntered:N,onExit:Id,onExiting:j,onExited:Bd,manager:ln(),transition:m?bv:void 0,backdropTransition:m?ey:void 0,renderBackdrop:Ud,renderDialog:Hd})})});_d.displayName="Modal";const tt=Object.assign(_d,{Body:Qv,Header:Zv,Title:qv,Footer:Gv,Dialog:xd,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ty(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...m},p){return v.jsx("div",{ref:p,...m,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${ty(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?v.jsx("span",{className:"visually-hidden",children:r}):r})}const jd=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p,className:g,children:w,...k}=r;return v.jsx("div",{ref:n,...k,className:M(g,m),children:w?cv(w,R=>y.cloneElement(R,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p},n)})});jd.displayName="ProgressBar";const ny=jd,Rd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=Df(),u=If(),s=`${o}-cols`,a=[];return i.forEach(d=>{const m=r[d];delete r[d];let p;m!=null&&typeof m=="object"?{cols:p}=m:p=m;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),v.jsx(n,{ref:l,...r,className:M(t,o,...a)})});Rd.displayName="Row";const Ld=Rd,Od=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return v.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Od.displayName="Spinner";const An=Od,ry=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",Sl="initializing",wa="paused",Pd="live",ly="error",vt=async(e,t,n)=>{console.log(e,t);const r=ry+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ft={listTorrents:()=>vt("GET","/torrents"),getTorrentDetails:e=>vt("GET",`/torrents/${e}`),getTorrentStats:e=>vt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),vt("POST",n,e)},pause:e=>vt("POST",`/torrents/${e}/pause`),start:e=>vt("POST",`/torrents/${e}/start`),forget:e=>vt("POST",`/torrents/${e}/forget`),delete:e=>vt("POST",`/torrents/${e}/delete`)},Vn=y.createContext(null),Fd=y.createContext(null),Go=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return v.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},oy=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Vn),d=()=>{l(!1),i(null),s(!1),n()},m=()=>{s(!0),(r?ft.delete:ft.forget)(e).then(()=>{a.refreshTorrents(),d()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return v.jsxs(tt,{show:t,onHide:d,children:[v.jsx(tt.Header,{closeButton:!0,children:"Delete torrent"}),v.jsxs(tt.Body,{children:[v.jsx(On,{children:v.jsx(On.Group,{controlId:"delete-torrent",children:v.jsx(On.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&v.jsx(zr,{error:o})]}),v.jsxs(tt.Footer,{children:[u&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:m,disabled:u,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:d,children:"Cancel"})]})]})},iy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Fd);const s=n=="live",a=n=="paused",d=y.useContext(Vn),m=()=>{l(!0),ft.start(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error starting torrent id=${e}`,details:k})}).finally(()=>l(!1))},p=()=>{l(!0),ft.pause(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error pausing torrent id=${e}`,details:k})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},w=()=>{l(!1),i(!1)};return v.jsx(Ld,{children:v.jsxs(Vu,{children:[a&&v.jsx(Go,{className:"bi-play-circle",onClick:m,disabled:r,color:"success"}),s&&v.jsx(Go,{className:"bi-pause-circle",onClick:p,disabled:r}),v.jsx(Go,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),v.jsx(oy,{id:e,show:o,onHide:w})]})})},uy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Sl||r==Pd)&&!u,d=l?"Error":`${s.toFixed(2)}%`,m=l?"danger":u?"success":r==Sl?"warning":"primary",p=()=>{var R;let k=(R=n==null?void 0:n.live)==null?void 0:R.snapshot.peer_stats;return k?`${k.live} / ${k.seen}`:""},g=()=>{var k;if(u)return"Completed";switch(r){case wa:return"Paused";case Sl:return"Checking files";case ly:return"Error"}return((k=n.live)==null?void 0:k.download_speed.human_readable)??"N/A"};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),v.jsxs(Ld,{className:w.join(" "),children:[v.jsx(yt,{size:3,label:"Name",children:t?v.jsxs(v.Fragment,{children:[v.jsx("div",{className:"text-truncate",children:yy(t)}),l&&v.jsxs("p",{className:"text-danger",children:[v.jsx("strong",{children:"Error:"})," ",l]})]}):v.jsx(An,{})}),n?v.jsxs(v.Fragment,{children:[v.jsx(yt,{label:"Size",children:`${zd(o)} `}),v.jsx(yt,{size:2,label:(r==wa,"Progress"),children:v.jsx(ny,{now:s,label:d,animated:a,variant:m})}),v.jsx(yt,{size:2,label:"Down Speed",children:g()}),v.jsx(yt,{label:"ETA",children:gy(n)}),v.jsx(yt,{size:2,label:"Peers",children:p()}),v.jsx(yt,{label:"Actions",children:v.jsx(iy,{id:e,statsResponse:n})})]}):v.jsx(yt,{label:"Loading stats",size:8,children:v.jsx(An,{})})]})},yt=({size:e,label:t,children:n})=>v.jsxs(Vu,{md:e||1,className:"py-3",children:[v.jsx("div",{className:"fw-bold",children:t}),n]}),sy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(0),s=()=>{u(i+1)};return y.useEffect(()=>{if(n===null)return Sy(async()=>{await ft.getTorrentDetails(t.id).then(r)},1e3)},[n]),y.useEffect(()=>$d(async()=>ft.getTorrentStats(t.id).then(g=>(o(g),g)).then(g=>g.finished?1e4:g.state==Sl||g.state==Pd?1e3:1e4,g=>1e4),0),[i]),v.jsx(Fd.Provider,{value:{refresh:s},children:v.jsx(uy,{id:e,detailsResponse:n,statsResponse:l})})},ay=e=>{if(e.torrents===null&&e.loading)return v.jsx(An,{});if(e.torrents!==null)return e.torrents.length===0?v.jsx("div",{className:"text-center",children:v.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):v.jsx(v.Fragment,{children:e.torrents.map(t=>v.jsx(sy,{id:t.id,torrent:t},t.id))})},cy=()=>{const[e,t]=y.useState(null),[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(!1),s=async()=>{u(!0);let d=await ft.listTorrents().finally(()=>u(!1));o(d.torrents)};y.useEffect(()=>$d(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return v.jsx(Vn.Provider,{value:a,children:v.jsxs("div",{className:"text-center",children:[v.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 0.0.1-alpha"}),v.jsx(vy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},fy=e=>{let{details:t}=e;return t?v.jsxs(v.Fragment,{children:[t.status&&v.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},zr=e=>{let{error:t,remove:n}=e;return t==null?null:v.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[v.jsx(fa.Heading,{children:t.text}),v.jsx(fy,{details:t.details})]})},Md=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState([]),[a,d]=y.useState(null);y.useContext(Vn);const m=n!==null||a!==null;y.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const w=await ft.uploadTorrent(n,{listOnly:!0});s(w.details.files)}catch(w){d({text:"Error uploading torrent",details:w})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return v.jsxs(v.Fragment,{children:[v.jsx(Mr,{variant:l,onClick:t,className:"m-1",children:e}),v.jsx(my,{show:m,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},dy=()=>{let[e,t]=y.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return v.jsx(Md,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},py=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return v.jsxs(v.Fragment,{children:[v.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),v.jsx(Md,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},my=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=y.useState([]),[a,d]=y.useState(!1),[m,p]=y.useState(null),g=y.useContext(Vn);y.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const w=()=>{n(),s([]),p(null),d(!1)},k=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},R=async()=>{d(!0),ft.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return v.jsxs(tt,{show:t,onHide:w,size:"lg",children:[v.jsx(tt.Header,{closeButton:!0,children:!!l||v.jsx(tt.Title,{children:"Select Files"})}),v.jsxs(tt.Body,{children:[o?v.jsx(An,{}):l?v.jsx(zr,{error:l}):v.jsx(On,{children:r.map((f,c)=>v.jsx(On.Group,{controlId:`check-${c}`,children:v.jsx(On.Check,{type:"checkbox",label:`${f.name} (${zd(f.length)})`,checked:u.includes(c),onChange:()=>k(c)})},c))}),v.jsx(zr,{error:m})]}),v.jsxs(tt.Footer,{children:[a&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:R,disabled:o||a||u.length==0,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:w,children:"Cancel"})]})]})},hy=()=>v.jsxs("div",{id:"buttons-container",className:"mt-3",children:[v.jsx(dy,{}),v.jsx(py,{})]}),vy=e=>{let t=y.useContext(Vn);return v.jsxs(pv,{children:[v.jsx(zr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),v.jsx(zr,{error:e.otherError}),v.jsx(ay,{torrents:e.torrents,loading:e.torrentsLoading}),v.jsx(hy,{})]})};function zd(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function yy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function gy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":wy(t)}function wy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function $d(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function Sy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function ky(){const e=document.getElementById("app");Yo.createRoot(e).render(v.jsx(y.StrictMode,{children:v.jsx(cy,{})}))}document.addEventListener("DOMContentLoaded",ky); +*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function mh(e){var t=hh(e,"string");return typeof t=="symbol"?t:String(t)}function hh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function vh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var d=arguments.length,m=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Bh(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Uh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function mo(e,t){return y.useMemo(()=>Uh(e,t),[e,t])}function Hh(e){return e&&"setState"in e?Cn.findDOMNode(e):e??null}const Wh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const m=y.useRef(null),p=mo(m,s),g=N=>{p(Hh(N))},w=N=>T=>{N&&m.current&&N(m.current,T)},k=y.useCallback(w(e),[e]),R=y.useCallback(w(t),[t]),f=y.useCallback(w(n),[n]),c=y.useCallback(w(r),[r]),h=y.useCallback(w(l),[l]),S=y.useCallback(w(o),[o]),C=y.useCallback(w(i),[i]);return v.jsx(zh,{ref:d,...a,onEnter:k,onEntered:f,onEntering:R,onExit:c,onExited:S,onExiting:h,addEndListener:C,nodeRef:m,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Wt.cloneElement(u,{ref:g})})}),Vh=Wh;function Qh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Qh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Qf=e=>y.forwardRef((t,n)=>v.jsx("div",{...t,ref:n,className:M(t.className,e)})),Kf=Qf("h4");Kf.displayName="DivStyledAsH4";const Gf=y.forwardRef(({className:e,bsPrefix:t,as:n=Kf,...r},l)=>(t=H(t,"alert-heading"),v.jsx(n,{ref:l,className:M(e,t),...r})));Gf.displayName="AlertHeading";const Kh=Gf;function Gh(){return y.useState(null)}function Yh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Xh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Zh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Jh=typeof document<"u",ca=Jh||Zh?y.useLayoutEffect:y.useEffect,qh=["as","disabled"];function bh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function ev(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&ev(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},m=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:m},a]}const tv=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=bh(e,qh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return v.jsx(i,Object.assign({},l,o,{ref:t}))});tv.displayName="Button";const nv=["onKeyDown"];function rv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function lv(e){return!e||e.trim()==="#"}const Yf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=rv(e,nv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return lv(r.href)||r.role==="button"?v.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):v.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Yf.displayName="Anchor";const ov=Yf,Xf=y.forwardRef(({className:e,bsPrefix:t,as:n=ov,...r},l)=>(t=H(t,"alert-link"),v.jsx(n,{ref:l,className:M(e,t),...r})));Xf.displayName="AlertLink";const iv=Xf,uv={[St]:"show",[Ht]:"show"},Zf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Bh(s),r==null||r(s,a)},[r]);return v.jsx(Vh,{ref:o,addEndListener:Ah,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,uv[s],n[s])})})});Zf.displayName="Fade";const Kl=Zf,sv={"aria-label":ot.string,onClick:ot.func,variant:ot.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>v.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=sv;const Jf=Wu,qf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:m=Kl,...p}=yh(e,{show:"onClose"}),g=H(n,"alert"),w=Pe(f=>{a&&a(!1,f)}),k=m===!0?Kl:m,R=v.jsxs("div",{role:"alert",...k?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&v.jsx(Jf,{onClick:w,"aria-label":l,variant:o}),u]});return k?v.jsx(k,{unmountOnExit:!0,...p,ref:void 0,in:r,children:R}):r?R:null});qf.displayName="Alert";const fa=Object.assign(qf,{Link:iv,Heading:Kh}),bf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:m}]=Hu({tagName:e,disabled:o,...u}),p=m;return v.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});bf.displayName="Button";const Mr=bf;function av(e){const t=y.useRef(e);return t.current=e,t}function ed(e){const t=av(e);y.useEffect(()=>()=>t.current(),[])}function cv(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function fv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function dv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=Df(),o=If(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,m,p;typeof a=="object"&&a!=null?{span:d,offset:m,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),m!=null&&u.push(`offset${g}-${m}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const td=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=dv(e);return v.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});td.displayName="Col";const Vu=td,nd=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return v.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});nd.displayName="Container";const pv=nd;var mv=Function.prototype.bind.call(Function.prototype.call,[].slice);function cn(e,t){return mv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const hv="data-rr-ui-";function vv(e){return`${hv}${e}`}const rd=y.createContext(Wn?window:void 0);rd.Provider;function Qu(){return y.useContext(rd)}const yv={type:ot.string,tooltip:ot.bool,as:ot.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>v.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=yv;const ld=Ku,gv=y.createContext({}),ct=gv,od=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ct);return t=H(t,"form-check-input"),v.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});od.displayName="FormCheckInput";const id=od,ud=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-check-label"),v.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});ud.displayName="FormCheckLabel";const Gi=ud,sd=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:m,style:p,title:g="",type:w="checkbox",label:k,children:R,as:f="input",...c},h)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ct),C=y.useMemo(()=>({controlId:e||S}),[S,e]),N=!R&&k!=null&&k!==!1||fv(R,Gi),T=v.jsx(id,{...c,type:w==="switch"?"checkbox":w,ref:h,isValid:i,isInvalid:u,disabled:o,as:f});return v.jsx(ct.Provider,{value:C,children:v.jsx("div",{style:p,className:M(m,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,w==="switch"&&n),children:R||v.jsxs(v.Fragment,{children:[T,N&&v.jsx(Gi,{title:g,children:k}),a&&v.jsx(ld,{type:d,tooltip:s,children:a})]})})})});sd.displayName="FormCheck";const Gl=Object.assign(sd,{Input:id,Label:Gi}),ad=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...m},p)=>{const{controlId:g}=y.useContext(ct);return e=H(e,"form-control"),v.jsx(d,{...m,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});ad.displayName="FormControl";const wv=Object.assign(ad,{Feedback:ld}),cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),v.jsx(n,{ref:l,className:M(e,t),...r})));cd.displayName="FormFloating";const Sv=cd,fd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return v.jsx(ct.Provider,{value:l,children:v.jsx(t,{...n,ref:r})})});fd.displayName="FormGroup";const dd=fd,pd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ct);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?v.jsx(Vu,{ref:u,as:"label",className:d,htmlFor:o,...i}):v.jsx(e,{ref:u,className:d,htmlFor:o,...i})});pd.displayName="FormLabel";const kv=pd,md=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-range"),v.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});md.displayName="FormRange";const Ev=md,hd=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ct);return e=H(e,"form-select"),v.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});hd.displayName="FormSelect";const xv=hd,vd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),v.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));vd.displayName="FormText";const Cv=vd,yd=y.forwardRef((e,t)=>v.jsx(Gl,{...e,ref:t,type:"switch"}));yd.displayName="Switch";const Nv=Object.assign(yd,{Input:Gl.Input,Label:Gl.Label}),gd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),v.jsxs(dd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,v.jsx("label",{htmlFor:r,children:l})]})));gd.displayName="FloatingLabel";const Tv=gd,_v={_ref:ot.any,validated:ot.bool,as:ot.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>v.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=_v;const On=Object.assign(Gu,{Group:dd,Control:wv,Floating:Sv,Check:Gl,Switch:Nv,Label:kv,Text:Cv,Range:Ev,Select:xv,FloatingLabel:Tv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Wn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Wo(e){e===void 0&&(e=po());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function jv(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=vv("modal-open");class Rv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return jv(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=Rv,Vo=(e,t)=>Wn?e==null?(t||po()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Lv(e,t){const n=Qu(),[r,l]=y.useState(()=>Vo(e,n==null?void 0:n.document));if(!r){const o=Vo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Vo(e);o!==r&&l(o)},[e,r]),r}function Ov({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Pe(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=mo(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Pv({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Pe(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Fv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Pv({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,m=>{throw a.in||i(!0),m})}}),s=mo(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?v.jsx(e,Object.assign({},n)):t?v.jsx(Fv,Object.assign({},n,{transition:t})):v.jsx(Ov,Object.assign({},n))}function Mv(e){return e.code==="Escape"||e.keyCode===27}const zv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function $v(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Qo;function Dv(e){return Qo||(Qo=new Yu({ownerDocument:e==null?void 0:e.document})),Qo}function Iv(e){const t=Qu(),n=e||Dv(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const wd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:m,runTransition:p,backdropTransition:g,runBackdropTransition:w,autoFocus:k=!0,enforceFocus:R=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:h,renderBackdrop:S=K=>v.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:j=()=>{},onExit:B,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:rn}=e,Qn=$v(e,zv);const _e=Qu(),Ke=Lv(N),x=Iv(C),L=Yh(),O=Xh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>x,[x]),Wn&&!O&&n&&(fe.current=Wo(_e==null?void 0:_e.document)),n&&D&&A(!1);const je=Pe(()=>{if(x.add(),on.current=Ql(document,"keydown",ho),ln.current=Ql(document,"focus",()=>setTimeout(Re),!0),T&&T(),k){var K,Hr;const Yn=Wo((K=(Hr=x.dialog)==null?void 0:Hr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Yn&&!da(x.dialog,Yn)&&(fe.current=Yn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),on.current==null||on.current(),ln.current==null||ln.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ke||je()},[n,Ke,je]),y.useEffect(()=>{D&&qe()},[D,qe]),ed(()=>{qe()});const Re=Pe(()=>{if(!R||!L()||!x.isTopModal())return;const K=Wo(_e==null?void 0:_e.document);x.dialog&&K&&!da(x.dialog,K)&&x.dialog.focus()}),mt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),ho=Pe(K=>{s&&Mv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||j())}),ln=y.useRef(),on=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const Ur=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=h?h(Ur):v.jsx("div",Object.assign({},Ur,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(m,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:B,onExiting:ie,onExited:Kn,onEnter:Ve,onEntering:Qe,onEntered:rn,children:Gn});let It=null;return u&&(It=S({ref:x.setBackdropRef,onClick:mt}),It=ha(g,w,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),v.jsx(v.Fragment,{children:Cn.createPortal(v.jsxs(v.Fragment,{children:[It,Gn]}),Ke)})});wd.displayName="Modal";const Av=Object.assign(wd,{Manager:Yu});function Bv(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Uv(e,t){e.classList?e.classList.add(t):Bv(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Hv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const fn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Wv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Uv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Hv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Ko;function Vv(e){return Ko||(Ko=new Wv(e)),Ko}const Sd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),v.jsx(n,{ref:l,className:M(e,t),...r})));Sd.displayName="ModalBody";const Qv=Sd,Kv=y.createContext({onHide(){}}),kd=Kv,Ed=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,m=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return v.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&m),children:v.jsx("div",{className:M(`${e}-content`,n),children:i})})});Ed.displayName="ModalDialog";const xd=Ed,Cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),v.jsx(n,{ref:l,className:M(e,t),...r})));Cd.displayName="ModalFooter";const Gv=Cd,Yv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(kd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return v.jsxs("div",{ref:i,...o,children:[l,n&&v.jsx(Jf,{"aria-label":e,variant:t,onClick:s})]})}),Xv=Yv,Nd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),v.jsx(Xv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Nd.displayName="ModalHeader";const Zv=Nd,Jv=Qf("h4"),Td=y.forwardRef(({className:e,bsPrefix:t,as:n=Jv,...r},l)=>(t=H(t,"modal-title"),v.jsx(n,{ref:l,className:M(e,t),...r})));Td.displayName="ModalTitle";const qv=Td;function bv(e){return v.jsx(Kl,{...e,timeout:null})}function ey(e){return v.jsx(Kl,{...e,timeout:null})}const _d=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=xd,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:m=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:w,onShow:k,onHide:R,container:f,autoFocus:c=!0,enforceFocus:h=!0,restoreFocus:S=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:j,onEnter:B,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...rn},Qn)=>{const[_e,Ke]=y.useState({}),[x,L]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,je]=Gh(),qe=mo(Qn,je),Re=Pe(R),mt=kh();e=H(e,"modal");const ho=y.useMemo(()=>({onHide:Re}),[Re]);function ln(){return Qe||Vv({isRTL:mt})}function on($){if(!Wn)return;const un=ln().getScrollbarWidth()>0,Zu=$.scrollHeight>po($).documentElement.clientHeight;Ke({paddingRight:un&&!Zu?pa():void 0,paddingLeft:!un&&Zu?pa():void 0})}const Kn=Pe(()=>{fe&&on(fe.dialog)});ed(()=>{Ki(window,"resize",Kn),A.current==null||A.current()});const Ur=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{L(!0),A.current=Vf(fe.dialog,()=>{L(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}R==null||R()},Yn=$=>{g?w==null||w($):($.preventDefault(),p==="static"&&It())},Dd=($,un)=>{$&&on($),B==null||B($,un)},Id=$=>{A.current==null||A.current(),T==null||T($)},Ad=($,un)=>{P==null||P($,un),Wf(window,"resize",Kn)},Bd=$=>{$&&($.style.display=""),ie==null||ie($),Ki(window,"resize",Kn)},Ud=y.useCallback($=>v.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!m&&"show")}),[m,Ve,e]),Xu={...n,..._e};Xu.display="block";const Hd=$=>v.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,x&&`${e}-static`,!m&&"show"),onClick:p?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:v.jsx(i,{...rn,onMouseDown:Ur,className:r,contentClassName:l,children:o})});return v.jsx(kd.Provider,{value:ho,children:v.jsx(Av,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:h,restoreFocus:S,restoreFocusOptions:C,onEscapeKeyDown:Yn,onShow:k,onHide:R,onEnter:Dd,onEntering:Ad,onEntered:N,onExit:Id,onExiting:j,onExited:Bd,manager:ln(),transition:m?bv:void 0,backdropTransition:m?ey:void 0,renderBackdrop:Ud,renderDialog:Hd})})});_d.displayName="Modal";const tt=Object.assign(_d,{Body:Qv,Header:Zv,Title:qv,Footer:Gv,Dialog:xd,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ty(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...m},p){return v.jsx("div",{ref:p,...m,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${ty(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?v.jsx("span",{className:"visually-hidden",children:r}):r})}const jd=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p,className:g,children:w,...k}=r;return v.jsx("div",{ref:n,...k,className:M(g,m),children:w?cv(w,R=>y.cloneElement(R,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p},n)})});jd.displayName="ProgressBar";const ny=jd,Rd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=Df(),u=If(),s=`${o}-cols`,a=[];return i.forEach(d=>{const m=r[d];delete r[d];let p;m!=null&&typeof m=="object"?{cols:p}=m:p=m;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),v.jsx(n,{ref:l,...r,className:M(t,o,...a)})});Rd.displayName="Row";const Ld=Rd,Od=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return v.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Od.displayName="Spinner";const An=Od,ry=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",Sl="initializing",wa="paused",Pd="live",ly="error",vt=async(e,t,n)=>{console.log(e,t);const r=ry+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ft={listTorrents:()=>vt("GET","/torrents"),getTorrentDetails:e=>vt("GET",`/torrents/${e}`),getTorrentStats:e=>vt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),vt("POST",n,e)},pause:e=>vt("POST",`/torrents/${e}/pause`),start:e=>vt("POST",`/torrents/${e}/start`),forget:e=>vt("POST",`/torrents/${e}/forget`),delete:e=>vt("POST",`/torrents/${e}/delete`)},Vn=y.createContext(null),Fd=y.createContext(null),Go=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return v.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},oy=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Vn),d=()=>{l(!1),i(null),s(!1),n()},m=()=>{s(!0),(r?ft.delete:ft.forget)(e).then(()=>{a.refreshTorrents(),d()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return v.jsxs(tt,{show:t,onHide:d,children:[v.jsx(tt.Header,{closeButton:!0,children:"Delete torrent"}),v.jsxs(tt.Body,{children:[v.jsx(On,{children:v.jsx(On.Group,{controlId:"delete-torrent",children:v.jsx(On.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&v.jsx(zr,{error:o})]}),v.jsxs(tt.Footer,{children:[u&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:m,disabled:u,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:d,children:"Cancel"})]})]})},iy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Fd);const s=n=="live",a=n=="paused"||n=="error",d=y.useContext(Vn),m=()=>{l(!0),ft.start(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error starting torrent id=${e}`,details:k})}).finally(()=>l(!1))},p=()=>{l(!0),ft.pause(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error pausing torrent id=${e}`,details:k})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},w=()=>{l(!1),i(!1)};return v.jsx(Ld,{children:v.jsxs(Vu,{children:[a&&v.jsx(Go,{className:"bi-play-circle",onClick:m,disabled:r,color:"success"}),s&&v.jsx(Go,{className:"bi-pause-circle",onClick:p,disabled:r}),v.jsx(Go,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),v.jsx(oy,{id:e,show:o,onHide:w})]})})},uy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Sl||r==Pd)&&!u,d=l?"Error":`${s.toFixed(2)}%`,m=l?"danger":u?"success":r==Sl?"warning":"primary",p=()=>{var R;let k=(R=n==null?void 0:n.live)==null?void 0:R.snapshot.peer_stats;return k?`${k.live} / ${k.seen}`:""},g=()=>{var k;if(u)return"Completed";switch(r){case wa:return"Paused";case Sl:return"Checking files";case ly:return"Error"}return((k=n.live)==null?void 0:k.download_speed.human_readable)??"N/A"};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),v.jsxs(Ld,{className:w.join(" "),children:[v.jsx(yt,{size:3,label:"Name",children:t?v.jsxs(v.Fragment,{children:[v.jsx("div",{className:"text-truncate",children:yy(t)}),l&&v.jsxs("p",{className:"text-danger",children:[v.jsx("strong",{children:"Error:"})," ",l]})]}):v.jsx(An,{})}),n?v.jsxs(v.Fragment,{children:[v.jsx(yt,{label:"Size",children:`${zd(o)} `}),v.jsx(yt,{size:2,label:(r==wa,"Progress"),children:v.jsx(ny,{now:s,label:d,animated:a,variant:m})}),v.jsx(yt,{size:2,label:"Down Speed",children:g()}),v.jsx(yt,{label:"ETA",children:gy(n)}),v.jsx(yt,{size:2,label:"Peers",children:p()}),v.jsx(yt,{label:"Actions",children:v.jsx(iy,{id:e,statsResponse:n})})]}):v.jsx(yt,{label:"Loading stats",size:8,children:v.jsx(An,{})})]})},yt=({size:e,label:t,children:n})=>v.jsxs(Vu,{md:e||1,className:"py-3",children:[v.jsx("div",{className:"fw-bold",children:t}),n]}),sy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(0),s=()=>{u(i+1)};return y.useEffect(()=>{if(n===null)return Sy(async()=>{await ft.getTorrentDetails(t.id).then(r)},1e3)},[n]),y.useEffect(()=>$d(async()=>ft.getTorrentStats(t.id).then(g=>(o(g),g)).then(g=>g.finished?1e4:g.state==Sl||g.state==Pd?1e3:1e4,g=>1e4),0),[i]),v.jsx(Fd.Provider,{value:{refresh:s},children:v.jsx(uy,{id:e,detailsResponse:n,statsResponse:l})})},ay=e=>{if(e.torrents===null&&e.loading)return v.jsx(An,{});if(e.torrents!==null)return e.torrents.length===0?v.jsx("div",{className:"text-center",children:v.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):v.jsx(v.Fragment,{children:e.torrents.map(t=>v.jsx(sy,{id:t.id,torrent:t},t.id))})},cy=()=>{const[e,t]=y.useState(null),[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(!1),s=async()=>{u(!0);let d=await ft.listTorrents().finally(()=>u(!1));o(d.torrents)};y.useEffect(()=>$d(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return v.jsx(Vn.Provider,{value:a,children:v.jsxs("div",{className:"text-center",children:[v.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 0.0.1-alpha"}),v.jsx(vy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},fy=e=>{let{details:t}=e;return t?v.jsxs(v.Fragment,{children:[t.status&&v.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},zr=e=>{let{error:t,remove:n}=e;return t==null?null:v.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[v.jsx(fa.Heading,{children:t.text}),v.jsx(fy,{details:t.details})]})},Md=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState([]),[a,d]=y.useState(null);y.useContext(Vn);const m=n!==null||a!==null;y.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const w=await ft.uploadTorrent(n,{listOnly:!0});s(w.details.files)}catch(w){d({text:"Error uploading torrent",details:w})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return v.jsxs(v.Fragment,{children:[v.jsx(Mr,{variant:l,onClick:t,className:"m-1",children:e}),v.jsx(my,{show:m,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},dy=()=>{let[e,t]=y.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return v.jsx(Md,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},py=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return v.jsxs(v.Fragment,{children:[v.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),v.jsx(Md,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},my=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=y.useState([]),[a,d]=y.useState(!1),[m,p]=y.useState(null),g=y.useContext(Vn);y.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const w=()=>{n(),s([]),p(null),d(!1)},k=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},R=async()=>{d(!0),ft.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return v.jsxs(tt,{show:t,onHide:w,size:"lg",children:[v.jsx(tt.Header,{closeButton:!0,children:!!l||v.jsx(tt.Title,{children:"Select Files"})}),v.jsxs(tt.Body,{children:[o?v.jsx(An,{}):l?v.jsx(zr,{error:l}):v.jsx(On,{children:r.map((f,c)=>v.jsx(On.Group,{controlId:`check-${c}`,children:v.jsx(On.Check,{type:"checkbox",label:`${f.name} (${zd(f.length)})`,checked:u.includes(c),onChange:()=>k(c)})},c))}),v.jsx(zr,{error:m})]}),v.jsxs(tt.Footer,{children:[a&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:R,disabled:o||a||u.length==0,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:w,children:"Cancel"})]})]})},hy=()=>v.jsxs("div",{id:"buttons-container",className:"mt-3",children:[v.jsx(dy,{}),v.jsx(py,{})]}),vy=e=>{let t=y.useContext(Vn);return v.jsxs(pv,{children:[v.jsx(zr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),v.jsx(zr,{error:e.otherError}),v.jsx(ay,{torrents:e.torrents,loading:e.torrentsLoading}),v.jsx(hy,{})]})};function zd(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function yy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function gy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":wy(t)}function wy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function $d(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function Sy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function ky(){const e=document.getElementById("app");Yo.createRoot(e).render(v.jsx(y.StrictMode,{children:v.jsx(cy,{})}))}document.addEventListener("DOMContentLoaded",ky); diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 9462266..1d4a3ec 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -106,7 +106,7 @@ const TorrentActions: React.FC<{ let refreshCtx = useContext(RefreshTorrentStatsContext); const canPause = state == 'live'; - const canUnpause = state == 'paused'; + const canUnpause = state == 'paused' || state == 'error'; const ctx = useContext(AppContext); From c6b4e78747bcb55c9caefc46dc65796af01a53a2 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 25 Nov 2023 19:20:47 +0000 Subject: [PATCH 34/38] Nothing much --- crates/librqbit/src/session.rs | 4 ++-- crates/librqbit/src/torrent_state/mod.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 2355518..c673eac 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -259,7 +259,7 @@ impl Session { let session = session.clone(); spawn( "session persistene", - error_span!("session persistence"), + error_span!("session_persistence"), async move { // Populate initial from the state filename if let Err(e) = session.populate_from_stored().await { @@ -290,7 +290,7 @@ impl Session { self.dht.as_ref() } - async fn populate_from_stored(&self) -> anyhow::Result<()> { + async fn populate_from_stored(self: &Arc) -> anyhow::Result<()> { let mut rdr = BufReader::new( std::fs::File::open(&self.persistence_filename).with_context(|| { format!("error opening session file {:?}", self.persistence_filename) diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index cd9f01d..1e9e72c 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -234,6 +234,7 @@ impl ManagedTorrent { } ManagedTorrentState::Initializing(init) => { let init = init.clone(); + drop(g); let t = self.clone(); let span = self.info().span.clone(); spawn( From 729252213941dc4de8e16978c7f1a274f4d56812 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sun, 26 Nov 2023 09:59:44 +0000 Subject: [PATCH 35/38] Session: add all at once --- TODO.md | 3 +- crates/librqbit/Cargo.toml | 2 +- crates/librqbit/src/session.rs | 54 +++++++++++++++++++--------------- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/TODO.md b/TODO.md index 987dd0d..0bb33c0 100644 --- a/TODO.md +++ b/TODO.md @@ -30,4 +30,5 @@ refactor: - [x] start from error state should be possible from UI - [ ] if the torrent was completed, not need to re-check it -- [ ] checking is very slow on raspberry \ No newline at end of file +- [ ] checking is very slow on raspberry +- [ ] .rqbit-session.json file has 0 bytes when disk full. I guess fs::rename does this when disk is full? at least on linux \ No newline at end of file diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index 1104017..9e18974 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -64,4 +64,4 @@ dashmap = "5.5.3" [dev-dependencies] futures = {version = "0.3"} -tracing-subscriber = "0.3" \ No newline at end of file +tracing-subscriber = "0.3" diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index c673eac..700fd2b 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -242,11 +242,11 @@ impl Session { Some(dht) }; let peer_opts = opts.peer_opts.unwrap_or_default(); - let session_filename = opts + let persistence_filename = opts .persistence_filename .unwrap_or_else(|| output_folder.join(".rqbit-session.json")); let session = Arc::new(Self { - persistence_filename: session_filename, + persistence_filename, peer_id, dht, peer_opts, @@ -298,34 +298,42 @@ impl Session { ); let db: SerializedSessionDatabase = serde_json::from_reader(&mut rdr).context("error deserializing session database")?; + let mut futures = Vec::new(); for storrent in db.torrents.into_iter() { let magnet = Magnet { info_hash: Id20::from_str(&storrent.info_hash) .context("error deserializing info_hash")?, trackers: storrent.trackers.into_iter().collect(), }; - if let Err(e) = self - .add_torrent( - AddTorrent::Url(Cow::Owned(magnet.to_string())), - Some(AddTorrentOptions { - paused: storrent.is_paused, - output_folder: Some( - storrent - .output_folder - .to_str() - .context("broken path")? - .to_owned(), - ), - only_files: storrent.only_files, - overwrite: true, - ..Default::default() - }), - ) - .await - { - error!("error adding torrent from stored session: {:?}", e) - } + futures.push({ + let session = self.clone(); + async move { + session + .add_torrent( + AddTorrent::Url(Cow::Owned(magnet.to_string())), + Some(AddTorrentOptions { + paused: storrent.is_paused, + output_folder: Some( + storrent + .output_folder + .to_str() + .context("broken path")? + .to_owned(), + ), + only_files: storrent.only_files, + overwrite: true, + ..Default::default() + }), + ) + .await + .map_err(|e| { + error!("error adding torrent from stored session: {:?}", e); + e + }) + } + }); } + futures::future::join_all(futures).await; Ok(()) } From f33346adfba7046780c1d8e825e4d16f94cba98e Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sun, 26 Nov 2023 10:26:49 +0000 Subject: [PATCH 36/38] Add armv7 build --- .cargo/config | 6 +++++- .github/workflows/release.yml | 10 ++++++++-- Makefile | 12 +++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.cargo/config b/.cargo/config index 0aca9d9..3dc7967 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,6 @@ [target.arm-unknown-linux-gnueabihf] -rustflags = ["-l", "atomic"] \ No newline at end of file +rustflags = ["-l", "atomic"] + +[target.armv7-unknown-linux-gnueabihf] +# Workaround for: https://github.com/rust-lang/compiler-builtins/issues/420 +rustflags = ["-C", "link-arg=-Wl,--allow-multiple-definition"] \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d22974..f6a46d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: - name: install linux cross compiler run: brew tap messense/macos-cross-toolchains && - brew install x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu arm-unknown-linux-gnueabihf + brew install x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu arm-unknown-linux-gnueabihf armv7-unknown-linux-gnueabihf - name: Make a directory for output artifacts run: @@ -41,12 +41,18 @@ jobs: make release-linux-x86_64 && mv target/x86_64-unknown-linux-gnu/release-github/rqbit target/artifacts/rqbit-linux-static-x86_64 - - name: Build release linux arm32bit binary + - name: Build release linux armv6 binary run: rustup target install arm-unknown-linux-gnueabihf && make release-linux-armv6 && mv target/arm-unknown-linux-gnueabihf/release-github/rqbit target/artifacts/rqbit-linux-static-arm32 + - name: Build release linux armv7 binary + run: + rustup target install armv7-unknown-linux-gnueabihf && + make release-linux-armv7 && + mv target/armv7-unknown-linux-gnueabihf/release-github/rqbit target/artifacts/rqbit-linux-static-arm32 + - name: Build release linux aarch64 binary run: rustup target install aarch64-unknown-linux-gnu && diff --git a/Makefile b/Makefile index 8bad46a..eeb5fc2 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ release-linux-current-target: target/openssl-linux/$(TARGET)/lib/libssl.a cargo build --profile release-github --target=$(TARGET) @PHONY: release-linux -release-linux: release-linux-x86_64 release-linux-aarch64 release-linux-armv6 +release-linux: release-linux-x86_64 release-linux-aarch64 release-linux-armv6 release-linux-armv7 @PHONY: release-linux-x86_64 release-linux-x86_64: @@ -105,6 +105,16 @@ release-linux-armv6: LDFLAGS=-latomic \ $(MAKE) release-linux-current-target +# armv7-unknown-linux-gnueabihf +@PHONY: release-linux-armv7 +release-linux-armv7: + TARGET=armv7-unknown-linux-gnueabihf \ + TARGET_SNAKE_CASE=armv7_unknown_linux_gnueabihf \ + TARGET_SNAKE_UPPER_CASE=ARMV7_UNKNOWN_LINUX_GNUEABIHF \ + CROSS_COMPILE_PREFIX=armv7-linux-gnueabihf \ + OPENSSL_CONFIGURE_NAME=linux-generic32 \ + $(MAKE) release-linux-current-target + @PHONY: release-all release-all: release-windows release-linux release-macos-universal From 1110eb8620a28c59266d517c14e2b7ae3d9088bf Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 27 Nov 2023 09:20:35 +0000 Subject: [PATCH 37/38] Create folders for session persistence --- TODO.md | 3 ++- crates/librqbit/src/session.rs | 21 +++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/TODO.md b/TODO.md index 0bb33c0..154b1eb 100644 --- a/TODO.md +++ b/TODO.md @@ -30,5 +30,6 @@ refactor: - [x] start from error state should be possible from UI - [ ] if the torrent was completed, not need to re-check it -- [ ] checking is very slow on raspberry +- [x] checking is very slow on raspberry + checked. nothing much can be done here. Even if raspberry's own libssl.so is used it's still super slow (sha1) - [ ] .rqbit-session.json file has 0 bytes when disk full. I guess fs::rename does this when disk is full? at least on linux \ No newline at end of file diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 700fd2b..d4a56bf 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -256,6 +256,11 @@ impl Session { }); if opts.persistence { + if let Some(parent) = session.persistence_filename.parent() { + std::fs::create_dir_all(parent).with_context(|| { + format!("couldn't create directory {:?} for session storage", parent) + })?; + } let session = session.clone(); spawn( "session persistene", @@ -291,11 +296,16 @@ impl Session { } async fn populate_from_stored(self: &Arc) -> anyhow::Result<()> { - let mut rdr = BufReader::new( - std::fs::File::open(&self.persistence_filename).with_context(|| { - format!("error opening session file {:?}", self.persistence_filename) - })?, - ); + let mut rdr = match std::fs::File::open(&self.persistence_filename) { + Ok(f) => BufReader::new(f), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(e) => { + return Err(e).context(format!( + "error opening session file {:?}", + self.persistence_filename + )) + } + }; let db: SerializedSessionDatabase = serde_json::from_reader(&mut rdr).context("error deserializing session database")?; let mut futures = Vec::new(); @@ -342,7 +352,6 @@ impl Session { let mut tmp = BufWriter::new( std::fs::OpenOptions::new() .create(true) - .create_new(true) .truncate(true) .write(true) .open(&tmp_filename) From 2e2492581efbf4af8fc884dffd5dcc447d43b2a2 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 27 Nov 2023 09:29:28 +0000 Subject: [PATCH 38/38] Update web UI version --- crates/librqbit/webui/dist/app.js | 2 +- crates/librqbit/webui/src/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/librqbit/webui/dist/app.js b/crates/librqbit/webui/dist/app.js index 4330772..f2f21b2 100644 --- a/crates/librqbit/webui/dist/app.js +++ b/crates/librqbit/webui/dist/app.js @@ -41,4 +41,4 @@ Error generating stack: `+o.message+` Copyright (c) 2018 Jed Watson. Licensed under the MIT License (MIT), see http://jedwatson.github.io/classnames -*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function mh(e){var t=hh(e,"string");return typeof t=="symbol"?t:String(t)}function hh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function vh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var d=arguments.length,m=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Bh(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Uh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function mo(e,t){return y.useMemo(()=>Uh(e,t),[e,t])}function Hh(e){return e&&"setState"in e?Cn.findDOMNode(e):e??null}const Wh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const m=y.useRef(null),p=mo(m,s),g=N=>{p(Hh(N))},w=N=>T=>{N&&m.current&&N(m.current,T)},k=y.useCallback(w(e),[e]),R=y.useCallback(w(t),[t]),f=y.useCallback(w(n),[n]),c=y.useCallback(w(r),[r]),h=y.useCallback(w(l),[l]),S=y.useCallback(w(o),[o]),C=y.useCallback(w(i),[i]);return v.jsx(zh,{ref:d,...a,onEnter:k,onEntered:f,onEntering:R,onExit:c,onExited:S,onExiting:h,addEndListener:C,nodeRef:m,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Wt.cloneElement(u,{ref:g})})}),Vh=Wh;function Qh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Qh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Qf=e=>y.forwardRef((t,n)=>v.jsx("div",{...t,ref:n,className:M(t.className,e)})),Kf=Qf("h4");Kf.displayName="DivStyledAsH4";const Gf=y.forwardRef(({className:e,bsPrefix:t,as:n=Kf,...r},l)=>(t=H(t,"alert-heading"),v.jsx(n,{ref:l,className:M(e,t),...r})));Gf.displayName="AlertHeading";const Kh=Gf;function Gh(){return y.useState(null)}function Yh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Xh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Zh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Jh=typeof document<"u",ca=Jh||Zh?y.useLayoutEffect:y.useEffect,qh=["as","disabled"];function bh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function ev(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&ev(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},m=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:m},a]}const tv=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=bh(e,qh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return v.jsx(i,Object.assign({},l,o,{ref:t}))});tv.displayName="Button";const nv=["onKeyDown"];function rv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function lv(e){return!e||e.trim()==="#"}const Yf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=rv(e,nv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return lv(r.href)||r.role==="button"?v.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):v.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Yf.displayName="Anchor";const ov=Yf,Xf=y.forwardRef(({className:e,bsPrefix:t,as:n=ov,...r},l)=>(t=H(t,"alert-link"),v.jsx(n,{ref:l,className:M(e,t),...r})));Xf.displayName="AlertLink";const iv=Xf,uv={[St]:"show",[Ht]:"show"},Zf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Bh(s),r==null||r(s,a)},[r]);return v.jsx(Vh,{ref:o,addEndListener:Ah,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,uv[s],n[s])})})});Zf.displayName="Fade";const Kl=Zf,sv={"aria-label":ot.string,onClick:ot.func,variant:ot.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>v.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=sv;const Jf=Wu,qf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:m=Kl,...p}=yh(e,{show:"onClose"}),g=H(n,"alert"),w=Pe(f=>{a&&a(!1,f)}),k=m===!0?Kl:m,R=v.jsxs("div",{role:"alert",...k?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&v.jsx(Jf,{onClick:w,"aria-label":l,variant:o}),u]});return k?v.jsx(k,{unmountOnExit:!0,...p,ref:void 0,in:r,children:R}):r?R:null});qf.displayName="Alert";const fa=Object.assign(qf,{Link:iv,Heading:Kh}),bf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:m}]=Hu({tagName:e,disabled:o,...u}),p=m;return v.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});bf.displayName="Button";const Mr=bf;function av(e){const t=y.useRef(e);return t.current=e,t}function ed(e){const t=av(e);y.useEffect(()=>()=>t.current(),[])}function cv(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function fv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function dv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=Df(),o=If(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,m,p;typeof a=="object"&&a!=null?{span:d,offset:m,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),m!=null&&u.push(`offset${g}-${m}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const td=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=dv(e);return v.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});td.displayName="Col";const Vu=td,nd=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return v.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});nd.displayName="Container";const pv=nd;var mv=Function.prototype.bind.call(Function.prototype.call,[].slice);function cn(e,t){return mv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const hv="data-rr-ui-";function vv(e){return`${hv}${e}`}const rd=y.createContext(Wn?window:void 0);rd.Provider;function Qu(){return y.useContext(rd)}const yv={type:ot.string,tooltip:ot.bool,as:ot.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>v.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=yv;const ld=Ku,gv=y.createContext({}),ct=gv,od=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ct);return t=H(t,"form-check-input"),v.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});od.displayName="FormCheckInput";const id=od,ud=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-check-label"),v.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});ud.displayName="FormCheckLabel";const Gi=ud,sd=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:m,style:p,title:g="",type:w="checkbox",label:k,children:R,as:f="input",...c},h)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ct),C=y.useMemo(()=>({controlId:e||S}),[S,e]),N=!R&&k!=null&&k!==!1||fv(R,Gi),T=v.jsx(id,{...c,type:w==="switch"?"checkbox":w,ref:h,isValid:i,isInvalid:u,disabled:o,as:f});return v.jsx(ct.Provider,{value:C,children:v.jsx("div",{style:p,className:M(m,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,w==="switch"&&n),children:R||v.jsxs(v.Fragment,{children:[T,N&&v.jsx(Gi,{title:g,children:k}),a&&v.jsx(ld,{type:d,tooltip:s,children:a})]})})})});sd.displayName="FormCheck";const Gl=Object.assign(sd,{Input:id,Label:Gi}),ad=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...m},p)=>{const{controlId:g}=y.useContext(ct);return e=H(e,"form-control"),v.jsx(d,{...m,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});ad.displayName="FormControl";const wv=Object.assign(ad,{Feedback:ld}),cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),v.jsx(n,{ref:l,className:M(e,t),...r})));cd.displayName="FormFloating";const Sv=cd,fd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return v.jsx(ct.Provider,{value:l,children:v.jsx(t,{...n,ref:r})})});fd.displayName="FormGroup";const dd=fd,pd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ct);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?v.jsx(Vu,{ref:u,as:"label",className:d,htmlFor:o,...i}):v.jsx(e,{ref:u,className:d,htmlFor:o,...i})});pd.displayName="FormLabel";const kv=pd,md=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-range"),v.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});md.displayName="FormRange";const Ev=md,hd=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ct);return e=H(e,"form-select"),v.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});hd.displayName="FormSelect";const xv=hd,vd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),v.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));vd.displayName="FormText";const Cv=vd,yd=y.forwardRef((e,t)=>v.jsx(Gl,{...e,ref:t,type:"switch"}));yd.displayName="Switch";const Nv=Object.assign(yd,{Input:Gl.Input,Label:Gl.Label}),gd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),v.jsxs(dd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,v.jsx("label",{htmlFor:r,children:l})]})));gd.displayName="FloatingLabel";const Tv=gd,_v={_ref:ot.any,validated:ot.bool,as:ot.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>v.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=_v;const On=Object.assign(Gu,{Group:dd,Control:wv,Floating:Sv,Check:Gl,Switch:Nv,Label:kv,Text:Cv,Range:Ev,Select:xv,FloatingLabel:Tv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Wn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Wo(e){e===void 0&&(e=po());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function jv(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=vv("modal-open");class Rv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return jv(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=Rv,Vo=(e,t)=>Wn?e==null?(t||po()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Lv(e,t){const n=Qu(),[r,l]=y.useState(()=>Vo(e,n==null?void 0:n.document));if(!r){const o=Vo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Vo(e);o!==r&&l(o)},[e,r]),r}function Ov({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Pe(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=mo(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Pv({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Pe(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Fv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Pv({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,m=>{throw a.in||i(!0),m})}}),s=mo(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?v.jsx(e,Object.assign({},n)):t?v.jsx(Fv,Object.assign({},n,{transition:t})):v.jsx(Ov,Object.assign({},n))}function Mv(e){return e.code==="Escape"||e.keyCode===27}const zv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function $v(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Qo;function Dv(e){return Qo||(Qo=new Yu({ownerDocument:e==null?void 0:e.document})),Qo}function Iv(e){const t=Qu(),n=e||Dv(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const wd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:m,runTransition:p,backdropTransition:g,runBackdropTransition:w,autoFocus:k=!0,enforceFocus:R=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:h,renderBackdrop:S=K=>v.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:j=()=>{},onExit:B,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:rn}=e,Qn=$v(e,zv);const _e=Qu(),Ke=Lv(N),x=Iv(C),L=Yh(),O=Xh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>x,[x]),Wn&&!O&&n&&(fe.current=Wo(_e==null?void 0:_e.document)),n&&D&&A(!1);const je=Pe(()=>{if(x.add(),on.current=Ql(document,"keydown",ho),ln.current=Ql(document,"focus",()=>setTimeout(Re),!0),T&&T(),k){var K,Hr;const Yn=Wo((K=(Hr=x.dialog)==null?void 0:Hr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Yn&&!da(x.dialog,Yn)&&(fe.current=Yn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),on.current==null||on.current(),ln.current==null||ln.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ke||je()},[n,Ke,je]),y.useEffect(()=>{D&&qe()},[D,qe]),ed(()=>{qe()});const Re=Pe(()=>{if(!R||!L()||!x.isTopModal())return;const K=Wo(_e==null?void 0:_e.document);x.dialog&&K&&!da(x.dialog,K)&&x.dialog.focus()}),mt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),ho=Pe(K=>{s&&Mv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||j())}),ln=y.useRef(),on=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const Ur=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=h?h(Ur):v.jsx("div",Object.assign({},Ur,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(m,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:B,onExiting:ie,onExited:Kn,onEnter:Ve,onEntering:Qe,onEntered:rn,children:Gn});let It=null;return u&&(It=S({ref:x.setBackdropRef,onClick:mt}),It=ha(g,w,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),v.jsx(v.Fragment,{children:Cn.createPortal(v.jsxs(v.Fragment,{children:[It,Gn]}),Ke)})});wd.displayName="Modal";const Av=Object.assign(wd,{Manager:Yu});function Bv(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Uv(e,t){e.classList?e.classList.add(t):Bv(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Hv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const fn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Wv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Uv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Hv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Ko;function Vv(e){return Ko||(Ko=new Wv(e)),Ko}const Sd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),v.jsx(n,{ref:l,className:M(e,t),...r})));Sd.displayName="ModalBody";const Qv=Sd,Kv=y.createContext({onHide(){}}),kd=Kv,Ed=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,m=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return v.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&m),children:v.jsx("div",{className:M(`${e}-content`,n),children:i})})});Ed.displayName="ModalDialog";const xd=Ed,Cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),v.jsx(n,{ref:l,className:M(e,t),...r})));Cd.displayName="ModalFooter";const Gv=Cd,Yv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(kd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return v.jsxs("div",{ref:i,...o,children:[l,n&&v.jsx(Jf,{"aria-label":e,variant:t,onClick:s})]})}),Xv=Yv,Nd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),v.jsx(Xv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Nd.displayName="ModalHeader";const Zv=Nd,Jv=Qf("h4"),Td=y.forwardRef(({className:e,bsPrefix:t,as:n=Jv,...r},l)=>(t=H(t,"modal-title"),v.jsx(n,{ref:l,className:M(e,t),...r})));Td.displayName="ModalTitle";const qv=Td;function bv(e){return v.jsx(Kl,{...e,timeout:null})}function ey(e){return v.jsx(Kl,{...e,timeout:null})}const _d=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=xd,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:m=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:w,onShow:k,onHide:R,container:f,autoFocus:c=!0,enforceFocus:h=!0,restoreFocus:S=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:j,onEnter:B,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...rn},Qn)=>{const[_e,Ke]=y.useState({}),[x,L]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,je]=Gh(),qe=mo(Qn,je),Re=Pe(R),mt=kh();e=H(e,"modal");const ho=y.useMemo(()=>({onHide:Re}),[Re]);function ln(){return Qe||Vv({isRTL:mt})}function on($){if(!Wn)return;const un=ln().getScrollbarWidth()>0,Zu=$.scrollHeight>po($).documentElement.clientHeight;Ke({paddingRight:un&&!Zu?pa():void 0,paddingLeft:!un&&Zu?pa():void 0})}const Kn=Pe(()=>{fe&&on(fe.dialog)});ed(()=>{Ki(window,"resize",Kn),A.current==null||A.current()});const Ur=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{L(!0),A.current=Vf(fe.dialog,()=>{L(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}R==null||R()},Yn=$=>{g?w==null||w($):($.preventDefault(),p==="static"&&It())},Dd=($,un)=>{$&&on($),B==null||B($,un)},Id=$=>{A.current==null||A.current(),T==null||T($)},Ad=($,un)=>{P==null||P($,un),Wf(window,"resize",Kn)},Bd=$=>{$&&($.style.display=""),ie==null||ie($),Ki(window,"resize",Kn)},Ud=y.useCallback($=>v.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!m&&"show")}),[m,Ve,e]),Xu={...n,..._e};Xu.display="block";const Hd=$=>v.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,x&&`${e}-static`,!m&&"show"),onClick:p?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:v.jsx(i,{...rn,onMouseDown:Ur,className:r,contentClassName:l,children:o})});return v.jsx(kd.Provider,{value:ho,children:v.jsx(Av,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:h,restoreFocus:S,restoreFocusOptions:C,onEscapeKeyDown:Yn,onShow:k,onHide:R,onEnter:Dd,onEntering:Ad,onEntered:N,onExit:Id,onExiting:j,onExited:Bd,manager:ln(),transition:m?bv:void 0,backdropTransition:m?ey:void 0,renderBackdrop:Ud,renderDialog:Hd})})});_d.displayName="Modal";const tt=Object.assign(_d,{Body:Qv,Header:Zv,Title:qv,Footer:Gv,Dialog:xd,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ty(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...m},p){return v.jsx("div",{ref:p,...m,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${ty(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?v.jsx("span",{className:"visually-hidden",children:r}):r})}const jd=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p,className:g,children:w,...k}=r;return v.jsx("div",{ref:n,...k,className:M(g,m),children:w?cv(w,R=>y.cloneElement(R,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p},n)})});jd.displayName="ProgressBar";const ny=jd,Rd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=Df(),u=If(),s=`${o}-cols`,a=[];return i.forEach(d=>{const m=r[d];delete r[d];let p;m!=null&&typeof m=="object"?{cols:p}=m:p=m;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),v.jsx(n,{ref:l,...r,className:M(t,o,...a)})});Rd.displayName="Row";const Ld=Rd,Od=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return v.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Od.displayName="Spinner";const An=Od,ry=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",Sl="initializing",wa="paused",Pd="live",ly="error",vt=async(e,t,n)=>{console.log(e,t);const r=ry+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ft={listTorrents:()=>vt("GET","/torrents"),getTorrentDetails:e=>vt("GET",`/torrents/${e}`),getTorrentStats:e=>vt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),vt("POST",n,e)},pause:e=>vt("POST",`/torrents/${e}/pause`),start:e=>vt("POST",`/torrents/${e}/start`),forget:e=>vt("POST",`/torrents/${e}/forget`),delete:e=>vt("POST",`/torrents/${e}/delete`)},Vn=y.createContext(null),Fd=y.createContext(null),Go=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return v.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},oy=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Vn),d=()=>{l(!1),i(null),s(!1),n()},m=()=>{s(!0),(r?ft.delete:ft.forget)(e).then(()=>{a.refreshTorrents(),d()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return v.jsxs(tt,{show:t,onHide:d,children:[v.jsx(tt.Header,{closeButton:!0,children:"Delete torrent"}),v.jsxs(tt.Body,{children:[v.jsx(On,{children:v.jsx(On.Group,{controlId:"delete-torrent",children:v.jsx(On.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&v.jsx(zr,{error:o})]}),v.jsxs(tt.Footer,{children:[u&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:m,disabled:u,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:d,children:"Cancel"})]})]})},iy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Fd);const s=n=="live",a=n=="paused"||n=="error",d=y.useContext(Vn),m=()=>{l(!0),ft.start(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error starting torrent id=${e}`,details:k})}).finally(()=>l(!1))},p=()=>{l(!0),ft.pause(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error pausing torrent id=${e}`,details:k})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},w=()=>{l(!1),i(!1)};return v.jsx(Ld,{children:v.jsxs(Vu,{children:[a&&v.jsx(Go,{className:"bi-play-circle",onClick:m,disabled:r,color:"success"}),s&&v.jsx(Go,{className:"bi-pause-circle",onClick:p,disabled:r}),v.jsx(Go,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),v.jsx(oy,{id:e,show:o,onHide:w})]})})},uy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Sl||r==Pd)&&!u,d=l?"Error":`${s.toFixed(2)}%`,m=l?"danger":u?"success":r==Sl?"warning":"primary",p=()=>{var R;let k=(R=n==null?void 0:n.live)==null?void 0:R.snapshot.peer_stats;return k?`${k.live} / ${k.seen}`:""},g=()=>{var k;if(u)return"Completed";switch(r){case wa:return"Paused";case Sl:return"Checking files";case ly:return"Error"}return((k=n.live)==null?void 0:k.download_speed.human_readable)??"N/A"};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),v.jsxs(Ld,{className:w.join(" "),children:[v.jsx(yt,{size:3,label:"Name",children:t?v.jsxs(v.Fragment,{children:[v.jsx("div",{className:"text-truncate",children:yy(t)}),l&&v.jsxs("p",{className:"text-danger",children:[v.jsx("strong",{children:"Error:"})," ",l]})]}):v.jsx(An,{})}),n?v.jsxs(v.Fragment,{children:[v.jsx(yt,{label:"Size",children:`${zd(o)} `}),v.jsx(yt,{size:2,label:(r==wa,"Progress"),children:v.jsx(ny,{now:s,label:d,animated:a,variant:m})}),v.jsx(yt,{size:2,label:"Down Speed",children:g()}),v.jsx(yt,{label:"ETA",children:gy(n)}),v.jsx(yt,{size:2,label:"Peers",children:p()}),v.jsx(yt,{label:"Actions",children:v.jsx(iy,{id:e,statsResponse:n})})]}):v.jsx(yt,{label:"Loading stats",size:8,children:v.jsx(An,{})})]})},yt=({size:e,label:t,children:n})=>v.jsxs(Vu,{md:e||1,className:"py-3",children:[v.jsx("div",{className:"fw-bold",children:t}),n]}),sy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(0),s=()=>{u(i+1)};return y.useEffect(()=>{if(n===null)return Sy(async()=>{await ft.getTorrentDetails(t.id).then(r)},1e3)},[n]),y.useEffect(()=>$d(async()=>ft.getTorrentStats(t.id).then(g=>(o(g),g)).then(g=>g.finished?1e4:g.state==Sl||g.state==Pd?1e3:1e4,g=>1e4),0),[i]),v.jsx(Fd.Provider,{value:{refresh:s},children:v.jsx(uy,{id:e,detailsResponse:n,statsResponse:l})})},ay=e=>{if(e.torrents===null&&e.loading)return v.jsx(An,{});if(e.torrents!==null)return e.torrents.length===0?v.jsx("div",{className:"text-center",children:v.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):v.jsx(v.Fragment,{children:e.torrents.map(t=>v.jsx(sy,{id:t.id,torrent:t},t.id))})},cy=()=>{const[e,t]=y.useState(null),[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(!1),s=async()=>{u(!0);let d=await ft.listTorrents().finally(()=>u(!1));o(d.torrents)};y.useEffect(()=>$d(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return v.jsx(Vn.Provider,{value:a,children:v.jsxs("div",{className:"text-center",children:[v.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 0.0.1-alpha"}),v.jsx(vy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},fy=e=>{let{details:t}=e;return t?v.jsxs(v.Fragment,{children:[t.status&&v.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},zr=e=>{let{error:t,remove:n}=e;return t==null?null:v.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[v.jsx(fa.Heading,{children:t.text}),v.jsx(fy,{details:t.details})]})},Md=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState([]),[a,d]=y.useState(null);y.useContext(Vn);const m=n!==null||a!==null;y.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const w=await ft.uploadTorrent(n,{listOnly:!0});s(w.details.files)}catch(w){d({text:"Error uploading torrent",details:w})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return v.jsxs(v.Fragment,{children:[v.jsx(Mr,{variant:l,onClick:t,className:"m-1",children:e}),v.jsx(my,{show:m,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},dy=()=>{let[e,t]=y.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return v.jsx(Md,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},py=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return v.jsxs(v.Fragment,{children:[v.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),v.jsx(Md,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},my=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=y.useState([]),[a,d]=y.useState(!1),[m,p]=y.useState(null),g=y.useContext(Vn);y.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const w=()=>{n(),s([]),p(null),d(!1)},k=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},R=async()=>{d(!0),ft.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return v.jsxs(tt,{show:t,onHide:w,size:"lg",children:[v.jsx(tt.Header,{closeButton:!0,children:!!l||v.jsx(tt.Title,{children:"Select Files"})}),v.jsxs(tt.Body,{children:[o?v.jsx(An,{}):l?v.jsx(zr,{error:l}):v.jsx(On,{children:r.map((f,c)=>v.jsx(On.Group,{controlId:`check-${c}`,children:v.jsx(On.Check,{type:"checkbox",label:`${f.name} (${zd(f.length)})`,checked:u.includes(c),onChange:()=>k(c)})},c))}),v.jsx(zr,{error:m})]}),v.jsxs(tt.Footer,{children:[a&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:R,disabled:o||a||u.length==0,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:w,children:"Cancel"})]})]})},hy=()=>v.jsxs("div",{id:"buttons-container",className:"mt-3",children:[v.jsx(dy,{}),v.jsx(py,{})]}),vy=e=>{let t=y.useContext(Vn);return v.jsxs(pv,{children:[v.jsx(zr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),v.jsx(zr,{error:e.otherError}),v.jsx(ay,{torrents:e.torrents,loading:e.torrentsLoading}),v.jsx(hy,{})]})};function zd(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function yy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function gy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":wy(t)}function wy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function $d(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function Sy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function ky(){const e=document.getElementById("app");Yo.createRoot(e).render(v.jsx(y.StrictMode,{children:v.jsx(cy,{})}))}document.addEventListener("DOMContentLoaded",ky); +*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function mh(e){var t=hh(e,"string");return typeof t=="symbol"?t:String(t)}function hh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function vh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var d=arguments.length,m=new Array(d>1?d-1:0),p=1;p{o.target===e&&(l(),t(o))},n+r)}function Bh(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Uh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function mo(e,t){return y.useMemo(()=>Uh(e,t),[e,t])}function Hh(e){return e&&"setState"in e?Cn.findDOMNode(e):e??null}const Wh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},d)=>{const m=y.useRef(null),p=mo(m,s),g=N=>{p(Hh(N))},w=N=>T=>{N&&m.current&&N(m.current,T)},k=y.useCallback(w(e),[e]),R=y.useCallback(w(t),[t]),f=y.useCallback(w(n),[n]),c=y.useCallback(w(r),[r]),h=y.useCallback(w(l),[l]),S=y.useCallback(w(o),[o]),C=y.useCallback(w(i),[i]);return v.jsx(zh,{ref:d,...a,onEnter:k,onEntered:f,onEntering:R,onExit:c,onExited:S,onExiting:h,addEndListener:C,nodeRef:m,children:typeof u=="function"?(N,T)=>u(N,{...T,ref:g}):Wt.cloneElement(u,{ref:g})})}),Vh=Wh;function Qh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Pe(e){const t=Qh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Qf=e=>y.forwardRef((t,n)=>v.jsx("div",{...t,ref:n,className:M(t.className,e)})),Kf=Qf("h4");Kf.displayName="DivStyledAsH4";const Gf=y.forwardRef(({className:e,bsPrefix:t,as:n=Kf,...r},l)=>(t=H(t,"alert-heading"),v.jsx(n,{ref:l,className:M(e,t),...r})));Gf.displayName="AlertHeading";const Kh=Gf;function Gh(){return y.useState(null)}function Yh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Xh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Zh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Jh=typeof document<"u",ca=Jh||Zh?y.useLayoutEffect:y.useEffect,qh=["as","disabled"];function bh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function ev(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const d=p=>{if((t||e==="a"&&ev(n))&&p.preventDefault(),t){p.stopPropagation();return}i==null||i(p)},m=p=>{p.key===" "&&(p.preventDefault(),d(p))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:d,onKeyDown:m},a]}const tv=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=bh(e,qh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return v.jsx(i,Object.assign({},l,o,{ref:t}))});tv.displayName="Button";const nv=["onKeyDown"];function rv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function lv(e){return!e||e.trim()==="#"}const Yf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=rv(e,nv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Pe(i=>{l.onKeyDown(i),n==null||n(i)});return lv(r.href)||r.role==="button"?v.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):v.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Yf.displayName="Anchor";const ov=Yf,Xf=y.forwardRef(({className:e,bsPrefix:t,as:n=ov,...r},l)=>(t=H(t,"alert-link"),v.jsx(n,{ref:l,className:M(e,t),...r})));Xf.displayName="AlertLink";const iv=Xf,uv={[St]:"show",[Ht]:"show"},Zf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Bh(s),r==null||r(s,a)},[r]);return v.jsx(Vh,{ref:o,addEndListener:Ah,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,uv[s],n[s])})})});Zf.displayName="Fade";const Kl=Zf,sv={"aria-label":ot.string,onClick:ot.func,variant:ot.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>v.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=sv;const Jf=Wu,qf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:d,transition:m=Kl,...p}=yh(e,{show:"onClose"}),g=H(n,"alert"),w=Pe(f=>{a&&a(!1,f)}),k=m===!0?Kl:m,R=v.jsxs("div",{role:"alert",...k?void 0:p,ref:t,className:M(i,g,s&&`${g}-${s}`,d&&`${g}-dismissible`),children:[d&&v.jsx(Jf,{onClick:w,"aria-label":l,variant:o}),u]});return k?v.jsx(k,{unmountOnExit:!0,...p,ref:void 0,in:r,children:R}):r?R:null});qf.displayName="Alert";const fa=Object.assign(qf,{Link:iv,Heading:Kh}),bf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[d,{tagName:m}]=Hu({tagName:e,disabled:o,...u}),p=m;return v.jsx(p,{...d,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});bf.displayName="Button";const Mr=bf;function av(e){const t=y.useRef(e);return t.current=e,t}function ed(e){const t=av(e);y.useEffect(()=>()=>t.current(),[])}function cv(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function fv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function dv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=Df(),o=If(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let d,m,p;typeof a=="object"&&a!=null?{span:d,offset:m,order:p}=a:d=a;const g=s!==o?`-${s}`:"";d&&i.push(d===!0?`${t}${g}`:`${t}${g}-${d}`),p!=null&&u.push(`order${g}-${p}`),m!=null&&u.push(`offset${g}-${m}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const td=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=dv(e);return v.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});td.displayName="Col";const Vu=td,nd=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return v.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});nd.displayName="Container";const pv=nd;var mv=Function.prototype.bind.call(Function.prototype.call,[].slice);function cn(e,t){return mv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const hv="data-rr-ui-";function vv(e){return`${hv}${e}`}const rd=y.createContext(Wn?window:void 0);rd.Provider;function Qu(){return y.useContext(rd)}const yv={type:ot.string,tooltip:ot.bool,as:ot.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>v.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=yv;const ld=Ku,gv=y.createContext({}),ct=gv,od=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ct);return t=H(t,"form-check-input"),v.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});od.displayName="FormCheckInput";const id=od,ud=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-check-label"),v.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});ud.displayName="FormCheckLabel";const Gi=ud,sd=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:d,className:m,style:p,title:g="",type:w="checkbox",label:k,children:R,as:f="input",...c},h)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ct),C=y.useMemo(()=>({controlId:e||S}),[S,e]),N=!R&&k!=null&&k!==!1||fv(R,Gi),T=v.jsx(id,{...c,type:w==="switch"?"checkbox":w,ref:h,isValid:i,isInvalid:u,disabled:o,as:f});return v.jsx(ct.Provider,{value:C,children:v.jsx("div",{style:p,className:M(m,N&&t,r&&`${t}-inline`,l&&`${t}-reverse`,w==="switch"&&n),children:R||v.jsxs(v.Fragment,{children:[T,N&&v.jsx(Gi,{title:g,children:k}),a&&v.jsx(ld,{type:d,tooltip:s,children:a})]})})})});sd.displayName="FormCheck";const Gl=Object.assign(sd,{Input:id,Label:Gi}),ad=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:d="input",...m},p)=>{const{controlId:g}=y.useContext(ct);return e=H(e,"form-control"),v.jsx(d,{...m,type:t,size:r,ref:p,readOnly:a,id:l||g,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});ad.displayName="FormControl";const wv=Object.assign(ad,{Feedback:ld}),cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),v.jsx(n,{ref:l,className:M(e,t),...r})));cd.displayName="FormFloating";const Sv=cd,fd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return v.jsx(ct.Provider,{value:l,children:v.jsx(t,{...n,ref:r})})});fd.displayName="FormGroup";const dd=fd,pd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ct);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const d=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?v.jsx(Vu,{ref:u,as:"label",className:d,htmlFor:o,...i}):v.jsx(e,{ref:u,className:d,htmlFor:o,...i})});pd.displayName="FormLabel";const kv=pd,md=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ct);return e=H(e,"form-range"),v.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});md.displayName="FormRange";const Ev=md,hd=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ct);return e=H(e,"form-select"),v.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});hd.displayName="FormSelect";const xv=hd,vd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),v.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));vd.displayName="FormText";const Cv=vd,yd=y.forwardRef((e,t)=>v.jsx(Gl,{...e,ref:t,type:"switch"}));yd.displayName="Switch";const Nv=Object.assign(yd,{Input:Gl.Input,Label:Gl.Label}),gd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),v.jsxs(dd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,v.jsx("label",{htmlFor:r,children:l})]})));gd.displayName="FloatingLabel";const Tv=gd,_v={_ref:ot.any,validated:ot.bool,as:ot.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>v.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=_v;const On=Object.assign(Gu,{Group:dd,Control:wv,Floating:Sv,Check:Gl,Switch:Nv,Label:kv,Text:Cv,Range:Ev,Select:xv,FloatingLabel:Tv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Wn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Wo(e){e===void 0&&(e=po());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function jv(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=vv("modal-open");class Rv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return jv(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=Rv,Vo=(e,t)=>Wn?e==null?(t||po()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Lv(e,t){const n=Qu(),[r,l]=y.useState(()=>Vo(e,n==null?void 0:n.document));if(!r){const o=Vo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Vo(e);o!==r&&l(o)},[e,r]),r}function Ov({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Pe(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=mo(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Pv({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Pe(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Fv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Pv({in:!!t,onTransition:a=>{const d=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(d,m=>{throw a.in||i(!0),m})}}),s=mo(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?v.jsx(e,Object.assign({},n)):t?v.jsx(Fv,Object.assign({},n,{transition:t})):v.jsx(Ov,Object.assign({},n))}function Mv(e){return e.code==="Escape"||e.keyCode===27}const zv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function $v(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Qo;function Dv(e){return Qo||(Qo=new Yu({ownerDocument:e==null?void 0:e.document})),Qo}function Iv(e){const t=Qu(),n=e||Dv(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const wd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:d,transition:m,runTransition:p,backdropTransition:g,runBackdropTransition:w,autoFocus:k=!0,enforceFocus:R=!0,restoreFocus:f=!0,restoreFocusOptions:c,renderDialog:h,renderBackdrop:S=K=>v.jsx("div",Object.assign({},K)),manager:C,container:N,onShow:T,onHide:j=()=>{},onExit:B,onExited:P,onExiting:ie,onEnter:Ve,onEntering:Qe,onEntered:rn}=e,Qn=$v(e,zv);const _e=Qu(),Ke=Lv(N),x=Iv(C),L=Yh(),O=Xh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>x,[x]),Wn&&!O&&n&&(fe.current=Wo(_e==null?void 0:_e.document)),n&&D&&A(!1);const je=Pe(()=>{if(x.add(),on.current=Ql(document,"keydown",ho),ln.current=Ql(document,"focus",()=>setTimeout(Re),!0),T&&T(),k){var K,Hr;const Yn=Wo((K=(Hr=x.dialog)==null?void 0:Hr.ownerDocument)!=null?K:_e==null?void 0:_e.document);x.dialog&&Yn&&!da(x.dialog,Yn)&&(fe.current=Yn,x.dialog.focus())}}),qe=Pe(()=>{if(x.remove(),on.current==null||on.current(),ln.current==null||ln.current(),f){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ke||je()},[n,Ke,je]),y.useEffect(()=>{D&&qe()},[D,qe]),ed(()=>{qe()});const Re=Pe(()=>{if(!R||!L()||!x.isTopModal())return;const K=Wo(_e==null?void 0:_e.document);x.dialog&&K&&!da(x.dialog,K)&&x.dialog.focus()}),mt=Pe(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),ho=Pe(K=>{s&&Mv(K)&&x.isTopModal()&&(d==null||d(K),K.defaultPrevented||j())}),ln=y.useRef(),on=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ke)return null;const Ur=Object.assign({role:r,ref:x.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=h?h(Ur):v.jsx("div",Object.assign({},Ur,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(m,p,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:B,onExiting:ie,onExited:Kn,onEnter:Ve,onEntering:Qe,onEntered:rn,children:Gn});let It=null;return u&&(It=S({ref:x.setBackdropRef,onClick:mt}),It=ha(g,w,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),v.jsx(v.Fragment,{children:Cn.createPortal(v.jsxs(v.Fragment,{children:[It,Gn]}),Ke)})});wd.displayName="Modal";const Av=Object.assign(wd,{Manager:Yu});function Bv(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Uv(e,t){e.classList?e.classList.add(t):Bv(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Hv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const fn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Wv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Uv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Hv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";cn(n,fn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),cn(n,fn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),cn(n,fn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Ko;function Vv(e){return Ko||(Ko=new Wv(e)),Ko}const Sd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),v.jsx(n,{ref:l,className:M(e,t),...r})));Sd.displayName="ModalBody";const Qv=Sd,Kv=y.createContext({onHide(){}}),kd=Kv,Ed=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const d=`${e}-dialog`,m=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return v.jsx("div",{...s,ref:a,className:M(d,t,l&&`${e}-${l}`,r&&`${d}-centered`,u&&`${d}-scrollable`,o&&m),children:v.jsx("div",{className:M(`${e}-content`,n),children:i})})});Ed.displayName="ModalDialog";const xd=Ed,Cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),v.jsx(n,{ref:l,className:M(e,t),...r})));Cd.displayName="ModalFooter";const Gv=Cd,Yv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(kd),s=Pe(()=>{u==null||u.onHide(),r==null||r()});return v.jsxs("div",{ref:i,...o,children:[l,n&&v.jsx(Jf,{"aria-label":e,variant:t,onClick:s})]})}),Xv=Yv,Nd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),v.jsx(Xv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Nd.displayName="ModalHeader";const Zv=Nd,Jv=Qf("h4"),Td=y.forwardRef(({className:e,bsPrefix:t,as:n=Jv,...r},l)=>(t=H(t,"modal-title"),v.jsx(n,{ref:l,className:M(e,t),...r})));Td.displayName="ModalTitle";const qv=Td;function bv(e){return v.jsx(Kl,{...e,timeout:null})}function ey(e){return v.jsx(Kl,{...e,timeout:null})}const _d=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=xd,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:d=!1,animation:m=!0,backdrop:p=!0,keyboard:g=!0,onEscapeKeyDown:w,onShow:k,onHide:R,container:f,autoFocus:c=!0,enforceFocus:h=!0,restoreFocus:S=!0,restoreFocusOptions:C,onEntered:N,onExit:T,onExiting:j,onEnter:B,onEntering:P,onExited:ie,backdropClassName:Ve,manager:Qe,...rn},Qn)=>{const[_e,Ke]=y.useState({}),[x,L]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,je]=Gh(),qe=mo(Qn,je),Re=Pe(R),mt=kh();e=H(e,"modal");const ho=y.useMemo(()=>({onHide:Re}),[Re]);function ln(){return Qe||Vv({isRTL:mt})}function on($){if(!Wn)return;const un=ln().getScrollbarWidth()>0,Zu=$.scrollHeight>po($).documentElement.clientHeight;Ke({paddingRight:un&&!Zu?pa():void 0,paddingLeft:!un&&Zu?pa():void 0})}const Kn=Pe(()=>{fe&&on(fe.dialog)});ed(()=>{Ki(window,"resize",Kn),A.current==null||A.current()});const Ur=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{L(!0),A.current=Vf(fe.dialog,()=>{L(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(p==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}R==null||R()},Yn=$=>{g?w==null||w($):($.preventDefault(),p==="static"&&It())},Dd=($,un)=>{$&&on($),B==null||B($,un)},Id=$=>{A.current==null||A.current(),T==null||T($)},Ad=($,un)=>{P==null||P($,un),Wf(window,"resize",Kn)},Bd=$=>{$&&($.style.display=""),ie==null||ie($),Ki(window,"resize",Kn)},Ud=y.useCallback($=>v.jsx("div",{...$,className:M(`${e}-backdrop`,Ve,!m&&"show")}),[m,Ve,e]),Xu={...n,..._e};Xu.display="block";const Hd=$=>v.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,x&&`${e}-static`,!m&&"show"),onClick:p?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:v.jsx(i,{...rn,onMouseDown:Ur,className:r,contentClassName:l,children:o})});return v.jsx(kd.Provider,{value:ho,children:v.jsx(Av,{show:d,ref:qe,backdrop:p,container:f,keyboard:!0,autoFocus:c,enforceFocus:h,restoreFocus:S,restoreFocusOptions:C,onEscapeKeyDown:Yn,onShow:k,onHide:R,onEnter:Dd,onEntering:Ad,onEntered:N,onExit:Id,onExiting:j,onExited:Bd,manager:ln(),transition:m?bv:void 0,backdropTransition:m?ey:void 0,renderBackdrop:Ud,renderDialog:Hd})})});_d.displayName="Modal";const tt=Object.assign(_d,{Body:Qv,Header:Zv,Title:qv,Footer:Gv,Dialog:xd,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ty(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:d,...m},p){return v.jsx("div",{ref:p,...m,role:"progressbar",className:M(u,`${d}-bar`,{[`bg-${a}`]:a,[`${d}-bar-animated`]:i,[`${d}-bar-striped`]:i||o}),style:{width:`${ty(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?v.jsx("span",{className:"visually-hidden",children:r}):r})}const jd=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p,className:g,children:w,...k}=r;return v.jsx("div",{ref:n,...k,className:M(g,m),children:w?cv(w,R=>y.cloneElement(R,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:d,bsPrefix:m,variant:p},n)})});jd.displayName="ProgressBar";const ny=jd,Rd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=Df(),u=If(),s=`${o}-cols`,a=[];return i.forEach(d=>{const m=r[d];delete r[d];let p;m!=null&&typeof m=="object"?{cols:p}=m:p=m;const g=d!==u?`-${d}`:"";p!=null&&a.push(`${s}${g}-${p}`)}),v.jsx(n,{ref:l,...r,className:M(t,o,...a)})});Rd.displayName="Row";const Ld=Rd,Od=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return v.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Od.displayName="Spinner";const An=Od,ry=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",Sl="initializing",wa="paused",Pd="live",ly="error",vt=async(e,t,n)=>{console.log(e,t);const r=ry+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=i.statusText,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ft={listTorrents:()=>vt("GET","/torrents"),getTorrentDetails:e=>vt("GET",`/torrents/${e}`),getTorrentStats:e=>vt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{t=t||{};let n="/torrents?&overwrite=true";return t.listOnly&&(n+="&list_only=true"),t.selectedFiles!=null&&(n+=`&only_files=${t.selectedFiles.join(",")}`),vt("POST",n,e)},pause:e=>vt("POST",`/torrents/${e}/pause`),start:e=>vt("POST",`/torrents/${e}/start`),forget:e=>vt("POST",`/torrents/${e}/forget`),delete:e=>vt("POST",`/torrents/${e}/delete`)},Vn=y.createContext(null),Fd=y.createContext(null),Go=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return v.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},oy=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Vn),d=()=>{l(!1),i(null),s(!1),n()},m=()=>{s(!0),(r?ft.delete:ft.forget)(e).then(()=>{a.refreshTorrents(),d()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return v.jsxs(tt,{show:t,onHide:d,children:[v.jsx(tt.Header,{closeButton:!0,children:"Delete torrent"}),v.jsxs(tt.Body,{children:[v.jsx(On,{children:v.jsx(On.Group,{controlId:"delete-torrent",children:v.jsx(On.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&v.jsx(zr,{error:o})]}),v.jsxs(tt.Footer,{children:[u&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:m,disabled:u,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:d,children:"Cancel"})]})]})},iy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Fd);const s=n=="live",a=n=="paused"||n=="error",d=y.useContext(Vn),m=()=>{l(!0),ft.start(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error starting torrent id=${e}`,details:k})}).finally(()=>l(!1))},p=()=>{l(!0),ft.pause(e).then(()=>{u.refresh()},k=>{d.setCloseableError({text:`Error pausing torrent id=${e}`,details:k})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},w=()=>{l(!1),i(!1)};return v.jsx(Ld,{children:v.jsxs(Vu,{children:[a&&v.jsx(Go,{className:"bi-play-circle",onClick:m,disabled:r,color:"success"}),s&&v.jsx(Go,{className:"bi-pause-circle",onClick:p,disabled:r}),v.jsx(Go,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),v.jsx(oy,{id:e,show:o,onHide:w})]})})},uy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Sl||r==Pd)&&!u,d=l?"Error":`${s.toFixed(2)}%`,m=l?"danger":u?"success":r==Sl?"warning":"primary",p=()=>{var R;let k=(R=n==null?void 0:n.live)==null?void 0:R.snapshot.peer_stats;return k?`${k.live} / ${k.seen}`:""},g=()=>{var k;if(u)return"Completed";switch(r){case wa:return"Paused";case Sl:return"Checking files";case ly:return"Error"}return((k=n.live)==null?void 0:k.download_speed.human_readable)??"N/A"};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),v.jsxs(Ld,{className:w.join(" "),children:[v.jsx(yt,{size:3,label:"Name",children:t?v.jsxs(v.Fragment,{children:[v.jsx("div",{className:"text-truncate",children:yy(t)}),l&&v.jsxs("p",{className:"text-danger",children:[v.jsx("strong",{children:"Error:"})," ",l]})]}):v.jsx(An,{})}),n?v.jsxs(v.Fragment,{children:[v.jsx(yt,{label:"Size",children:`${zd(o)} `}),v.jsx(yt,{size:2,label:(r==wa,"Progress"),children:v.jsx(ny,{now:s,label:d,animated:a,variant:m})}),v.jsx(yt,{size:2,label:"Down Speed",children:g()}),v.jsx(yt,{label:"ETA",children:gy(n)}),v.jsx(yt,{size:2,label:"Peers",children:p()}),v.jsx(yt,{label:"Actions",children:v.jsx(iy,{id:e,statsResponse:n})})]}):v.jsx(yt,{label:"Loading stats",size:8,children:v.jsx(An,{})})]})},yt=({size:e,label:t,children:n})=>v.jsxs(Vu,{md:e||1,className:"py-3",children:[v.jsx("div",{className:"fw-bold",children:t}),n]}),sy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(0),s=()=>{u(i+1)};return y.useEffect(()=>{if(n===null)return Sy(async()=>{await ft.getTorrentDetails(t.id).then(r)},1e3)},[n]),y.useEffect(()=>$d(async()=>ft.getTorrentStats(t.id).then(g=>(o(g),g)).then(g=>g.finished?1e4:g.state==Sl||g.state==Pd?1e3:1e4,g=>1e4),0),[i]),v.jsx(Fd.Provider,{value:{refresh:s},children:v.jsx(uy,{id:e,detailsResponse:n,statsResponse:l})})},ay=e=>{if(e.torrents===null&&e.loading)return v.jsx(An,{});if(e.torrents!==null)return e.torrents.length===0?v.jsx("div",{className:"text-center",children:v.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):v.jsx(v.Fragment,{children:e.torrents.map(t=>v.jsx(sy,{id:t.id,torrent:t},t.id))})},cy=()=>{const[e,t]=y.useState(null),[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(!1),s=async()=>{u(!0);let d=await ft.listTorrents().finally(()=>u(!1));o(d.torrents)};y.useEffect(()=>$d(async()=>s().then(()=>(r(null),5e3),d=>(r({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const a={setCloseableError:t,refreshTorrents:s};return v.jsx(Vn.Provider,{value:a,children:v.jsxs("div",{className:"text-center",children:[v.jsx("h1",{className:"mt-3 mb-4",children:"rqbit web 4.0.0-beta.0"}),v.jsx(vy,{closeableError:e,otherError:n,torrents:l,torrentsLoading:i})]})})},fy=e=>{let{details:t}=e;return t?v.jsxs(v.Fragment,{children:[t.status&&v.jsxs("strong",{children:[t.status," ",t.statusText,": "]}),t.text]}):null},zr=e=>{let{error:t,remove:n}=e;return t==null?null:v.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[v.jsx(fa.Heading,{children:t.text}),v.jsx(fy,{details:t.details})]})},Md=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState([]),[a,d]=y.useState(null);y.useContext(Vn);const m=n!==null||a!==null;y.useEffect(()=>{if(n===null)return;let g=setTimeout(async()=>{i(!0);try{const w=await ft.uploadTorrent(n,{listOnly:!0});s(w.details.files)}catch(w){d({text:"Error uploading torrent",details:w})}finally{i(!1)}},0);return()=>clearTimeout(g)},[n]);const p=()=>{r(),d(null),s([]),i(!1)};return v.jsxs(v.Fragment,{children:[v.jsx(Mr,{variant:l,onClick:t,className:"m-1",children:e}),v.jsx(my,{show:m,onHide:p,fileListError:a,fileList:u,data:n,fileListLoading:o})]})},dy=()=>{let[e,t]=y.useState(null);const n=()=>{const r=prompt("Enter magnet link or HTTP(s) URL");t(r===""?null:r)};return v.jsx(Md,{variant:"primary",buttonText:"Add Torrent from Magnet Link",onClick:n,data:e,resetData:()=>t(null)})},py=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{const i=e.current.files[0];n(i)},l=()=>{e.current.value="",n(null)},o=()=>{e.current.click()};return v.jsxs(v.Fragment,{children:[v.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),v.jsx(Md,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},my=e=>{let{show:t,onHide:n,fileList:r,fileListError:l,fileListLoading:o,data:i}=e;const[u,s]=y.useState([]),[a,d]=y.useState(!1),[m,p]=y.useState(null),g=y.useContext(Vn);y.useEffect(()=>{s(r.map((f,c)=>c))},[r]);const w=()=>{n(),s([]),p(null),d(!1)},k=f=>{u.includes(f)?s(u.filter(c=>c!==f)):s([...u,f])},R=async()=>{d(!0),ft.uploadTorrent(i,{selectedFiles:u}).then(()=>{n(),g.refreshTorrents()},f=>{p({text:"Error starting torrent",details:f})}).finally(()=>d(!1))};return v.jsxs(tt,{show:t,onHide:w,size:"lg",children:[v.jsx(tt.Header,{closeButton:!0,children:!!l||v.jsx(tt.Title,{children:"Select Files"})}),v.jsxs(tt.Body,{children:[o?v.jsx(An,{}):l?v.jsx(zr,{error:l}):v.jsx(On,{children:r.map((f,c)=>v.jsx(On.Group,{controlId:`check-${c}`,children:v.jsx(On.Check,{type:"checkbox",label:`${f.name} (${zd(f.length)})`,checked:u.includes(c),onChange:()=>k(c)})},c))}),v.jsx(zr,{error:m})]}),v.jsxs(tt.Footer,{children:[a&&v.jsx(An,{}),v.jsx(Mr,{variant:"primary",onClick:R,disabled:o||a||u.length==0,children:"OK"}),v.jsx(Mr,{variant:"secondary",onClick:w,children:"Cancel"})]})]})},hy=()=>v.jsxs("div",{id:"buttons-container",className:"mt-3",children:[v.jsx(dy,{}),v.jsx(py,{})]}),vy=e=>{let t=y.useContext(Vn);return v.jsxs(pv,{children:[v.jsx(zr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),v.jsx(zr,{error:e.otherError}),v.jsx(ay,{torrents:e.torrents,loading:e.torrentsLoading}),v.jsx(hy,{})]})};function zd(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function yy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function gy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":wy(t)}function wy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function $d(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function Sy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}async function ky(){const e=document.getElementById("app");Yo.createRoot(e).render(v.jsx(y.StrictMode,{children:v.jsx(cy,{})}))}document.addEventListener("DOMContentLoaded",ky); diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 1d4a3ec..efa10b0 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -340,7 +340,7 @@ const Root = () => { return
-

rqbit web 0.0.1-alpha

+

rqbit web 4.0.0-beta.0