forked from SerenityOS/serenity
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Userland: Add Bits, a BitTorrent GUI client based on LibBitTorrent
- Loading branch information
1 parent
d957b6c
commit 2a10d95
Showing
13 changed files
with
707 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
[App] | ||
Name=Bits | ||
Executable=/bin/Bits | ||
Category=Internet | ||
|
||
[Launcher] | ||
FileTypes=torrent |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
/* | ||
* Copyright (c) 2023, Pierre Delagrave <[email protected]> | ||
* | ||
* SPDX-License-Identifier: BSD-2-Clause | ||
*/ | ||
|
||
#include "BitsWidget.h" | ||
#include "GeneralTorrentInfoWidget.h" | ||
#include <AK/NumberFormat.h> | ||
#include <LibFileSystemAccessClient/Client.h> | ||
#include <LibGUI/Action.h> | ||
#include <LibGUI/Application.h> | ||
#include <LibGUI/BoxLayout.h> | ||
#include <LibGUI/Menu.h> | ||
#include <LibGUI/MessageBox.h> | ||
#include <LibGUI/SortingProxyModel.h> | ||
#include <LibGUI/Splitter.h> | ||
|
||
int TorrentModel::column_count(GUI::ModelIndex const&) const | ||
{ | ||
return Column::__Count; | ||
} | ||
|
||
ErrorOr<String> TorrentModel::column_name(int column) const | ||
{ | ||
switch (column) { | ||
case Column::Name: | ||
return "Name"_string; | ||
case Column::Size: | ||
return "Size"_string; | ||
case Column::State: | ||
return "State"_string; | ||
case Column::Progress: | ||
return "Progress"_string; | ||
case Column::DownloadSpeed: | ||
return "Download Speed"_string; | ||
case Column::UploadSpeed: | ||
return "Upload Speed"_string; | ||
case Column::Path: | ||
return "Path"_string; | ||
default: | ||
VERIFY_NOT_REACHED(); | ||
} | ||
} | ||
|
||
GUI::Variant TorrentModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const | ||
{ | ||
if (role == GUI::ModelRole::TextAlignment) | ||
return Gfx::TextAlignment::CenterLeft; | ||
if (role == GUI::ModelRole::Display) { | ||
auto torrent = torrent_at(index.row()); | ||
|
||
switch (index.column()) { | ||
case Column::Name: | ||
return torrent.display_name; | ||
case Column::Size: | ||
return AK::human_readable_quantity(torrent.size); | ||
case Column::State: | ||
return state_to_string(torrent.state); | ||
case Column::Progress: | ||
return DeprecatedString::formatted("{:.1}%", torrent.state == BitTorrent::TorrentState::CHECKING ? torrent.check_progress : torrent.progress); | ||
case Column::DownloadSpeed: | ||
return DeprecatedString::formatted("{}/s", human_readable_size(torrent.download_speed)); | ||
case Column::UploadSpeed: | ||
return DeprecatedString::formatted("{}/s", human_readable_size(torrent.upload_speed)); | ||
case Column::Path: | ||
return torrent.save_path; | ||
default: | ||
VERIFY_NOT_REACHED(); | ||
} | ||
} | ||
return {}; | ||
} | ||
|
||
int TorrentModel::row_count(GUI::ModelIndex const&) const | ||
{ | ||
return m_torrents->size(); | ||
} | ||
|
||
BitTorrent::TorrentView TorrentModel::torrent_at(int index) const | ||
{ | ||
return m_torrents->get(m_hashes.at(index)).release_value(); | ||
} | ||
|
||
void TorrentModel::update(NonnullOwnPtr<HashMap<BitTorrent::InfoHash, BitTorrent::TorrentView>> torrents) | ||
{ | ||
m_torrents = move(torrents); | ||
m_hashes = m_torrents->keys(); | ||
did_update(UpdateFlag::DontInvalidateIndices); | ||
} | ||
|
||
void BitsWidget::open_file(String const& filename, NonnullOwnPtr<Core::File> file, bool start) | ||
{ | ||
dbgln("Opening file {}", filename); | ||
auto maybe_meta_info = BitTorrent::MetaInfo::create(*file); | ||
file->close(); | ||
|
||
if (maybe_meta_info.is_error()) { | ||
GUI::MessageBox::show_error(this->window(), String::formatted("Error parsing torrent file: {}", maybe_meta_info.error()).release_value()); | ||
return; | ||
} | ||
|
||
auto meta_info = maybe_meta_info.release_value(); | ||
auto info_hash = BitTorrent::InfoHash(meta_info->info_hash()); | ||
m_engine->add_torrent(move(meta_info), Core::StandardPaths::downloads_directory()); | ||
|
||
if (start) | ||
m_engine->start_torrent(info_hash); | ||
} | ||
|
||
ErrorOr<NonnullRefPtr<BitsWidget>> BitsWidget::create(NonnullRefPtr<BitTorrent::Engine> engine, GUI::Window* window) | ||
{ | ||
auto widget = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) BitsWidget(move(engine)))); | ||
|
||
widget->set_layout<GUI::VerticalBoxLayout>(); | ||
|
||
auto file_menu = window->add_menu("&File"_string); | ||
|
||
file_menu->add_action(GUI::CommonActions::make_open_action([window, widget](auto&) { | ||
FileSystemAccessClient::OpenFileOptions options { | ||
.window_title = "Open a torrent file"sv, | ||
.path = Core::StandardPaths::home_directory(), | ||
.requested_access = Core::File::OpenMode::Read, | ||
.allowed_file_types = { { GUI::FileTypeFilter { "Torrent Files", { { "torrent" } } }, GUI::FileTypeFilter::all_files() } } | ||
}; | ||
auto maybe_file = FileSystemAccessClient::Client::the().open_file(window, options); | ||
if (maybe_file.is_error()) { | ||
dbgln("err: {}", maybe_file.error()); | ||
return; | ||
} | ||
|
||
widget->open_file(maybe_file.value().filename(), maybe_file.value().release_stream(), false); | ||
})); | ||
|
||
file_menu->add_action(GUI::CommonActions::make_quit_action([&](auto&) { | ||
GUI::Application::the()->quit(); | ||
})); | ||
|
||
auto start_torrent_action = GUI::Action::create("Start", | ||
[widget](GUI::Action&) { | ||
widget->m_torrents_table_view->selection().for_each_index([widget](GUI::ModelIndex const& index) { | ||
widget->m_engine->start_torrent(widget->m_torrent_model->torrent_at(index.row()).info_hash); | ||
}); | ||
}); | ||
|
||
auto stop_torrent_action = GUI::Action::create("Stop", | ||
[widget](GUI::Action&) { | ||
widget->m_torrents_table_view->selection().for_each_index([widget](GUI::ModelIndex const& index) { | ||
widget->m_engine->stop_torrent(widget->m_torrent_model->torrent_at(index.row()).info_hash); | ||
}); | ||
}); | ||
|
||
auto cancel_checking_torrent_action = GUI::Action::create("Cancel checking", | ||
[widget](GUI::Action&) { | ||
widget->m_torrents_table_view->selection().for_each_index([widget](GUI::ModelIndex const& index) { | ||
widget->m_engine->cancel_checking(widget->m_torrent_model->torrent_at(index.row()).info_hash); | ||
}); | ||
}); | ||
|
||
auto& main_splitter = widget->add<GUI::VerticalSplitter>(); | ||
main_splitter.layout()->set_spacing(4); | ||
|
||
widget->m_torrent_model = make_ref_counted<TorrentModel>(); | ||
widget->m_torrents_table_view = main_splitter.add<GUI::TableView>(); | ||
widget->m_torrents_table_view->set_model(widget->m_torrent_model); | ||
widget->m_torrents_table_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection); | ||
|
||
widget->m_torrents_table_view->on_context_menu_request = [widget, start_torrent_action, stop_torrent_action, cancel_checking_torrent_action](GUI::ModelIndex const& model_index, GUI::ContextMenuEvent const& event) { | ||
if (model_index.is_valid()) { | ||
widget->m_torrent_context_menu = GUI::Menu::construct(); | ||
BitTorrent::TorrentState state = widget->m_torrent_model->torrent_at(model_index.row()).state; | ||
if (state == BitTorrent::TorrentState::STOPPED || state == BitTorrent::TorrentState::ERROR) | ||
widget->m_torrent_context_menu->add_action(start_torrent_action); | ||
else if (state == BitTorrent::TorrentState::STARTED || state == BitTorrent::TorrentState::SEEDING) | ||
widget->m_torrent_context_menu->add_action(stop_torrent_action); | ||
else if (state == BitTorrent::TorrentState::CHECKING) | ||
widget->m_torrent_context_menu->add_action(cancel_checking_torrent_action); | ||
|
||
widget->m_torrent_context_menu->popup(event.screen_position()); | ||
} | ||
}; | ||
|
||
widget->m_bottom_tab_widget = main_splitter.add<GUI::TabWidget>(); | ||
widget->m_bottom_tab_widget->set_preferred_height(14); | ||
widget->m_general_widget = widget->m_bottom_tab_widget->add_tab<GeneralTorrentInfoWidget>("General"_string); | ||
widget->m_peer_list_widget = widget->m_bottom_tab_widget->add_tab<PeerListWidget>("Peers"_string); | ||
|
||
auto selected_torrent = [widget]() -> Optional<BitTorrent::TorrentView> { | ||
int selected_index = widget->m_torrents_table_view->selection().first().row(); | ||
if (selected_index >= 0) | ||
return widget->m_torrent_model->torrent_at(selected_index); | ||
else | ||
return {}; | ||
}; | ||
|
||
auto update_general_widget = [widget, selected_torrent] { | ||
widget->m_general_widget->update(selected_torrent()); | ||
}; | ||
|
||
auto update_peer_list_widget = [widget, selected_torrent] { | ||
auto peers = selected_torrent().map([&](auto torrent) -> auto { return torrent.peers; }).value_or({}); | ||
widget->m_peer_list_widget->update(peers); | ||
}; | ||
|
||
widget->m_torrents_table_view->on_selection_change = [update_peer_list_widget, update_general_widget] { | ||
update_general_widget(); | ||
update_peer_list_widget(); | ||
}; | ||
|
||
widget->m_engine->register_views_update_callback(200, [&, widget, &event_loop = Core::EventLoop::current(), update_general_widget, update_peer_list_widget](NonnullOwnPtr<HashMap<BitTorrent::InfoHash, BitTorrent::TorrentView>> torrents) { | ||
event_loop.deferred_invoke([&, widget, update_general_widget, update_peer_list_widget, torrents = move(torrents)]() mutable { | ||
u64 progress = 0; | ||
for (auto const& torrent : *torrents) { | ||
progress += torrent.value.progress; | ||
} | ||
widget->window()->set_progress(torrents->size() > 0 ? progress / torrents->size() : 0); | ||
widget->m_torrent_model->update(move(torrents)); | ||
update_general_widget(); | ||
update_peer_list_widget(); | ||
}); | ||
}); | ||
|
||
return widget; | ||
} | ||
|
||
BitsWidget::BitsWidget(NonnullRefPtr<BitTorrent::Engine> engine) | ||
: m_engine(move(engine)) | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/* | ||
* Copyright (c) 2023, Pierre Delagrave <[email protected]> | ||
* | ||
* SPDX-License-Identifier: BSD-2-Clause | ||
*/ | ||
|
||
#pragma once | ||
|
||
#include "GeneralTorrentInfoWidget.h" | ||
#include "PeerListWidget.h" | ||
#include <LibBitTorrent/Engine.h> | ||
#include <LibGUI/TableView.h> | ||
#include <LibGUI/Widget.h> | ||
|
||
class TorrentModel final : public GUI::Model { | ||
public: | ||
virtual int column_count(GUI::ModelIndex const& index = GUI::ModelIndex()) const override; | ||
virtual ErrorOr<String> column_name(int i) const override; | ||
virtual GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole role) const override; | ||
virtual int row_count(GUI::ModelIndex const& index = GUI::ModelIndex()) const override; | ||
|
||
BitTorrent::TorrentView torrent_at(int index) const; | ||
void update(NonnullOwnPtr<HashMap<BitTorrent::InfoHash, BitTorrent::TorrentView>>); | ||
|
||
private: | ||
enum Column { | ||
Name, | ||
Size, | ||
State, | ||
Progress, | ||
DownloadSpeed, | ||
UploadSpeed, | ||
Path, | ||
__Count | ||
}; | ||
|
||
NonnullOwnPtr<HashMap<BitTorrent::InfoHash, BitTorrent::TorrentView>> m_torrents { make<HashMap<BitTorrent::InfoHash, BitTorrent::TorrentView>>() }; | ||
Vector<BitTorrent::InfoHash> m_hashes; | ||
}; | ||
|
||
class BitsWidget final : public GUI::Widget { | ||
C_OBJECT(BitsWidget) | ||
public: | ||
static ErrorOr<NonnullRefPtr<BitsWidget>> create(NonnullRefPtr<BitTorrent::Engine> engine, GUI::Window* window); | ||
virtual ~BitsWidget() override = default; | ||
void open_file(String const& filename, NonnullOwnPtr<Core::File>, bool start); | ||
|
||
private: | ||
BitsWidget(NonnullRefPtr<BitTorrent::Engine>); | ||
RefPtr<GUI::Menu> m_torrent_context_menu; | ||
|
||
RefPtr<GUI::TableView> m_torrents_table_view; | ||
RefPtr<TorrentModel> m_torrent_model; | ||
RefPtr<Core::Timer> m_update_timer; | ||
|
||
RefPtr<GUI::TabWidget> m_bottom_tab_widget; | ||
RefPtr<GeneralTorrentInfoWidget> m_general_widget; | ||
RefPtr<PeerListWidget> m_peer_list_widget; | ||
|
||
NonnullRefPtr<BitTorrent::Engine> m_engine; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
serenity_component( | ||
Bits | ||
RECOMMENDED | ||
TARGETS Bits | ||
) | ||
|
||
set(SOURCES | ||
BitsWidget.cpp | ||
GeneralTorrentInfoWidget.cpp | ||
PeerListWidget.cpp | ||
main.cpp | ||
) | ||
|
||
serenity_app(Bits ICON hard-disk) | ||
target_link_libraries(Bits PRIVATE LibCore LibGfx LibGUI LibDesktop LibFileSystemAccessClient LibMain LibBitTorrent) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* Copyright (c) 2023, Pierre Delagrave <[email protected]> | ||
* | ||
* SPDX-License-Identifier: BSD-2-Clause | ||
*/ | ||
|
||
#include "GeneralTorrentInfoWidget.h" | ||
#include <LibGUI/BoxLayout.h> | ||
#include <LibGUI/Painter.h> | ||
|
||
TorrentProgressBar::TorrentProgressBar() | ||
{ | ||
set_fixed_height(20); | ||
} | ||
|
||
void TorrentProgressBar::update(Optional<BitTorrent::BitField> bitfield) | ||
{ | ||
m_bitfield = bitfield; | ||
GUI::Widget::update(); | ||
} | ||
|
||
void TorrentProgressBar::paint_event(GUI::PaintEvent&) | ||
{ | ||
GUI::Painter painter(*this); | ||
painter.clear_rect(rect(), Color::White); | ||
|
||
if (!m_bitfield.has_value()) | ||
return; | ||
|
||
int piece_width = max(rect().width() / m_bitfield->size(), 1); | ||
for (int i = 0; i < (int)m_bitfield->size(); i++) { | ||
if (m_bitfield->get(i)) | ||
painter.fill_rect({ i * piece_width, 0, piece_width, height() }, Color::Blue); | ||
} | ||
} | ||
|
||
GeneralTorrentInfoWidget::GeneralTorrentInfoWidget() | ||
{ | ||
set_layout<GUI::VerticalBoxLayout>(4); | ||
m_progress_bar = add<TorrentProgressBar>(); | ||
} | ||
void GeneralTorrentInfoWidget::update(Optional<BitTorrent::TorrentView> torrent) | ||
{ | ||
if (torrent.has_value()) | ||
m_progress_bar->update(torrent.value().bitfield); | ||
else | ||
m_progress_bar->update({}); | ||
} |
Oops, something went wrong.