|
|
|
@@ -21,12 +21,8 @@ namespace telegram_tui {
|
|
|
|
namespace {
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
|
|
constexpr std::array<AttachmentType, 7> kAttachmentTypes = {
|
|
|
|
constexpr std::array<AttachmentType, 7> kAttachmentTypes = {
|
|
|
|
AttachmentType::Photo,
|
|
|
|
AttachmentType::Photo, AttachmentType::Video, AttachmentType::Document,
|
|
|
|
AttachmentType::Video,
|
|
|
|
AttachmentType::Audio, AttachmentType::Voice, AttachmentType::Animation,
|
|
|
|
AttachmentType::Document,
|
|
|
|
|
|
|
|
AttachmentType::Audio,
|
|
|
|
|
|
|
|
AttachmentType::Voice,
|
|
|
|
|
|
|
|
AttachmentType::Animation,
|
|
|
|
|
|
|
|
AttachmentType::Sticker,
|
|
|
|
AttachmentType::Sticker,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@@ -60,8 +56,8 @@ std::filesystem::path downloads_directory() {
|
|
|
|
|
|
|
|
|
|
|
|
std::string sanitize_filename(std::string value) {
|
|
|
|
std::string sanitize_filename(std::string value) {
|
|
|
|
for (char &ch : value) {
|
|
|
|
for (char &ch : value) {
|
|
|
|
if (ch == '/' || ch == '\\' || ch == ':' || ch == '*' || ch == '?' || ch == '"' || ch == '<' ||
|
|
|
|
if (ch == '/' || ch == '\\' || ch == ':' || ch == '*' || ch == '?' || ch == '"' ||
|
|
|
|
ch == '>' || ch == '|') {
|
|
|
|
ch == '<' || ch == '>' || ch == '|') {
|
|
|
|
ch = '_';
|
|
|
|
ch = '_';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -80,7 +76,8 @@ std::filesystem::path preferred_attachment_filename(const AttachmentInfo& attach
|
|
|
|
|
|
|
|
|
|
|
|
std::filesystem::path candidate(filename);
|
|
|
|
std::filesystem::path candidate(filename);
|
|
|
|
if (!candidate.has_extension() && !attachment.local_path.empty()) {
|
|
|
|
if (!candidate.has_extension() && !attachment.local_path.empty()) {
|
|
|
|
const std::string extension = std::filesystem::path(attachment.local_path).extension().string();
|
|
|
|
const std::string extension =
|
|
|
|
|
|
|
|
std::filesystem::path(attachment.local_path).extension().string();
|
|
|
|
if (!extension.empty()) {
|
|
|
|
if (!extension.empty()) {
|
|
|
|
candidate += extension;
|
|
|
|
candidate += extension;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -88,13 +85,14 @@ std::filesystem::path preferred_attachment_filename(const AttachmentInfo& attach
|
|
|
|
return candidate;
|
|
|
|
return candidate;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::filesystem::path unique_destination_path(
|
|
|
|
std::filesystem::path unique_destination_path(const std::filesystem::path &directory,
|
|
|
|
const std::filesystem::path& directory, const std::filesystem::path& filename) {
|
|
|
|
const std::filesystem::path &filename) {
|
|
|
|
const std::filesystem::path stem = filename.stem();
|
|
|
|
const std::filesystem::path stem = filename.stem();
|
|
|
|
const std::filesystem::path extension = filename.extension();
|
|
|
|
const std::filesystem::path extension = filename.extension();
|
|
|
|
std::filesystem::path candidate = directory / filename;
|
|
|
|
std::filesystem::path candidate = directory / filename;
|
|
|
|
for (int index = 1; std::filesystem::exists(candidate); ++index) {
|
|
|
|
for (int index = 1; std::filesystem::exists(candidate); ++index) {
|
|
|
|
candidate = directory / (stem.string() + " (" + std::to_string(index) + ")" + extension.string());
|
|
|
|
candidate = directory / (stem.string() + " (" + std::to_string(index) + ")" +
|
|
|
|
|
|
|
|
extension.string());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return candidate;
|
|
|
|
return candidate;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -108,7 +106,8 @@ std::string inline_preview_unavailable_message(const AttachmentInfo& attachment)
|
|
|
|
if (!std::filesystem::exists(attachment.local_path)) {
|
|
|
|
if (!std::filesystem::exists(attachment.local_path)) {
|
|
|
|
return "Downloaded photo file is missing on disk.";
|
|
|
|
return "Downloaded photo file is missing on disk.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "No supported preview backend found. Install `kitten`, `img2sixel`, or `chafa`.";
|
|
|
|
return "No supported preview backend found. Install `kitten`, `img2sixel`, or "
|
|
|
|
|
|
|
|
"`chafa`.";
|
|
|
|
case AttachmentType::Video:
|
|
|
|
case AttachmentType::Video:
|
|
|
|
if (!attachment.is_downloaded || attachment.local_path.empty()) {
|
|
|
|
if (!attachment.is_downloaded || attachment.local_path.empty()) {
|
|
|
|
return "Video is not downloaded yet.";
|
|
|
|
return "Video is not downloaded yet.";
|
|
|
|
@@ -116,7 +115,8 @@ std::string inline_preview_unavailable_message(const AttachmentInfo& attachment)
|
|
|
|
if (!std::filesystem::exists(attachment.local_path)) {
|
|
|
|
if (!std::filesystem::exists(attachment.local_path)) {
|
|
|
|
return "Downloaded video file is missing on disk.";
|
|
|
|
return "Downloaded video file is missing on disk.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "Video preview requires `ffmpegthumbnailer` or `ffmpeg`, plus `kitten`, `img2sixel`, or `chafa`.";
|
|
|
|
return "Video preview requires `ffmpegthumbnailer` or `ffmpeg`, plus `kitten`, "
|
|
|
|
|
|
|
|
"`img2sixel`, or `chafa`.";
|
|
|
|
default:
|
|
|
|
default:
|
|
|
|
return "Inline preview is only supported for photos and videos.";
|
|
|
|
return "Inline preview is only supported for photos and videos.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -142,7 +142,8 @@ std::string attachment_progress_bar(const AttachmentInfo& attachment, int width)
|
|
|
|
if (attachment.is_downloaded) {
|
|
|
|
if (attachment.is_downloaded) {
|
|
|
|
filled = inner_width;
|
|
|
|
filled = inner_width;
|
|
|
|
} else if (attachment.size_bytes > 0 && attachment.downloaded_size > 0) {
|
|
|
|
} else if (attachment.size_bytes > 0 && attachment.downloaded_size > 0) {
|
|
|
|
filled = std::clamp((inner_width * attachment_progress_percent(attachment)) / 100, 0, inner_width);
|
|
|
|
filled = std::clamp((inner_width * attachment_progress_percent(attachment)) / 100,
|
|
|
|
|
|
|
|
0, inner_width);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "[" + std::string(filled, '#') + std::string(inner_width - filled, '.') + "]";
|
|
|
|
return "[" + std::string(filled, '#') + std::string(inner_width - filled, '.') + "]";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -209,7 +210,8 @@ bool command_exists(const char* command) {
|
|
|
|
return cached->second;
|
|
|
|
return cached->second;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::string resolved = run_command_capture("command -v " + std::string(command) + " 2>/dev/null");
|
|
|
|
const std::string resolved =
|
|
|
|
|
|
|
|
run_command_capture("command -v " + std::string(command) + " 2>/dev/null");
|
|
|
|
const bool exists = !trim_copy(resolved).empty();
|
|
|
|
const bool exists = !trim_copy(resolved).empty();
|
|
|
|
cache[command] = exists;
|
|
|
|
cache[command] = exists;
|
|
|
|
return exists;
|
|
|
|
return exists;
|
|
|
|
@@ -320,8 +322,8 @@ bool terminal_supports_sixels() {
|
|
|
|
const std::string term = lower_copy(get_env("TERM"));
|
|
|
|
const std::string term = lower_copy(get_env("TERM"));
|
|
|
|
const std::string term_program = lower_copy(get_env("TERM_PROGRAM"));
|
|
|
|
const std::string term_program = lower_copy(get_env("TERM_PROGRAM"));
|
|
|
|
return term == "foot" || term.find("xterm") != std::string::npos ||
|
|
|
|
return term == "foot" || term.find("xterm") != std::string::npos ||
|
|
|
|
term.find("mlterm") != std::string::npos || term.find("wezterm") != std::string::npos ||
|
|
|
|
term.find("mlterm") != std::string::npos ||
|
|
|
|
term_program == "wezterm";
|
|
|
|
term.find("wezterm") != std::string::npos || term_program == "wezterm";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct TerminalWindowSize {
|
|
|
|
struct TerminalWindowSize {
|
|
|
|
@@ -403,7 +405,8 @@ void App::sync_message_attachment_selection() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message_attachment_message_id_ = 0;
|
|
|
|
message_attachment_message_id_ = 0;
|
|
|
|
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend(); ++it) {
|
|
|
|
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend();
|
|
|
|
|
|
|
|
++it) {
|
|
|
|
if (it->has_attachment) {
|
|
|
|
if (it->has_attachment) {
|
|
|
|
message_attachment_message_id_ = it->id;
|
|
|
|
message_attachment_message_id_ = it->id;
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
@@ -438,9 +441,7 @@ bool App::move_message_attachment_selection(int delta) {
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto selected = std::find(
|
|
|
|
auto selected = std::find(attachment_message_ids.begin(), attachment_message_ids.end(),
|
|
|
|
attachment_message_ids.begin(),
|
|
|
|
|
|
|
|
attachment_message_ids.end(),
|
|
|
|
|
|
|
|
message_attachment_message_id_);
|
|
|
|
message_attachment_message_id_);
|
|
|
|
if (selected == attachment_message_ids.end()) {
|
|
|
|
if (selected == attachment_message_ids.end()) {
|
|
|
|
message_attachment_message_id_ = attachment_message_ids.back();
|
|
|
|
message_attachment_message_id_ = attachment_message_ids.back();
|
|
|
|
@@ -449,11 +450,13 @@ bool App::move_message_attachment_selection(int delta) {
|
|
|
|
|
|
|
|
|
|
|
|
const std::ptrdiff_t index = selected - attachment_message_ids.begin();
|
|
|
|
const std::ptrdiff_t index = selected - attachment_message_ids.begin();
|
|
|
|
const std::ptrdiff_t next_index = index + (delta < 0 ? -1 : 1);
|
|
|
|
const std::ptrdiff_t next_index = index + (delta < 0 ? -1 : 1);
|
|
|
|
if (next_index < 0 || next_index >= static_cast<std::ptrdiff_t>(attachment_message_ids.size())) {
|
|
|
|
if (next_index < 0 ||
|
|
|
|
|
|
|
|
next_index >= static_cast<std::ptrdiff_t>(attachment_message_ids.size())) {
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message_attachment_message_id_ = attachment_message_ids[static_cast<std::size_t>(next_index)];
|
|
|
|
message_attachment_message_id_ =
|
|
|
|
|
|
|
|
attachment_message_ids[static_cast<std::size_t>(next_index)];
|
|
|
|
return true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -502,7 +505,8 @@ std::optional<AttachmentInfo> App::selected_attachment() const {
|
|
|
|
|
|
|
|
|
|
|
|
const AttachmentType selected_type = kAttachmentTypes[category_index];
|
|
|
|
const AttachmentType selected_type = kAttachmentTypes[category_index];
|
|
|
|
std::vector<AttachmentInfo> attachments;
|
|
|
|
std::vector<AttachmentInfo> attachments;
|
|
|
|
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend(); ++it) {
|
|
|
|
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend();
|
|
|
|
|
|
|
|
++it) {
|
|
|
|
if (it->has_attachment && it->attachment.type == selected_type) {
|
|
|
|
if (it->has_attachment && it->attachment.type == selected_type) {
|
|
|
|
attachments.push_back(it->attachment);
|
|
|
|
attachments.push_back(it->attachment);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -522,7 +526,8 @@ std::optional<AttachmentInfo> App::selected_attachment() const {
|
|
|
|
return attachments[static_cast<std::size_t>(selection_index)];
|
|
|
|
return attachments[static_cast<std::size_t>(selection_index)];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string App::render_attachment_preview(const AttachmentInfo& attachment, int width, int height) const {
|
|
|
|
std::string App::render_attachment_preview(const AttachmentInfo &attachment, int width,
|
|
|
|
|
|
|
|
int height) const {
|
|
|
|
const std::string preview_path = attachment_preview_path(attachment);
|
|
|
|
const std::string preview_path = attachment_preview_path(attachment);
|
|
|
|
if (preview_path.empty()) {
|
|
|
|
if (preview_path.empty()) {
|
|
|
|
return inline_preview_unavailable_message(attachment);
|
|
|
|
return inline_preview_unavailable_message(attachment);
|
|
|
|
@@ -530,9 +535,10 @@ std::string App::render_attachment_preview(const AttachmentInfo& attachment, int
|
|
|
|
|
|
|
|
|
|
|
|
const int render_width = std::max(10, width);
|
|
|
|
const int render_width = std::max(10, width);
|
|
|
|
const int render_height = std::max(4, height);
|
|
|
|
const int render_height = std::max(4, height);
|
|
|
|
const std::string command =
|
|
|
|
const std::string command = "command -v chafa >/dev/null 2>&1 && chafa --format symbols "
|
|
|
|
"command -v chafa >/dev/null 2>&1 && chafa --format symbols --symbols all --colors none --size " +
|
|
|
|
"--symbols all --colors none --size " +
|
|
|
|
std::to_string(render_width) + "x" + std::to_string(render_height) + " " +
|
|
|
|
std::to_string(render_width) + "x" +
|
|
|
|
|
|
|
|
std::to_string(render_height) + " " +
|
|
|
|
shell_quote(preview_path) + " 2>/dev/null";
|
|
|
|
shell_quote(preview_path) + " 2>/dev/null";
|
|
|
|
std::string preview = run_command_capture(command);
|
|
|
|
std::string preview = run_command_capture(command);
|
|
|
|
if (!preview.empty()) {
|
|
|
|
if (!preview.empty()) {
|
|
|
|
@@ -541,29 +547,32 @@ std::string App::render_attachment_preview(const AttachmentInfo& attachment, int
|
|
|
|
return "No supported preview backend found. Install `kitten`, `img2sixel`, or `chafa`.";
|
|
|
|
return "No supported preview backend found. Install `kitten`, `img2sixel`, or `chafa`.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string App::build_attachment_preview_graphics(const AttachmentInfo& attachment, int width, int height) const {
|
|
|
|
std::string App::build_attachment_preview_graphics(const AttachmentInfo &attachment, int width,
|
|
|
|
|
|
|
|
int height) const {
|
|
|
|
const std::string preview_path = attachment_preview_path(attachment);
|
|
|
|
const std::string preview_path = attachment_preview_path(attachment);
|
|
|
|
if (preview_path.empty()) {
|
|
|
|
if (preview_path.empty()) {
|
|
|
|
return {};
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::string forced_protocol = configured_image_protocol();
|
|
|
|
const std::string forced_protocol = configured_image_protocol();
|
|
|
|
const bool want_sixels =
|
|
|
|
const bool want_sixels = forced_protocol == "sixels" ||
|
|
|
|
forced_protocol == "sixels" || (forced_protocol.empty() && terminal_supports_sixels());
|
|
|
|
(forced_protocol.empty() && terminal_supports_sixels());
|
|
|
|
if (want_sixels) {
|
|
|
|
if (want_sixels) {
|
|
|
|
if (command_exists("chafa")) {
|
|
|
|
if (command_exists("chafa")) {
|
|
|
|
return "chafa --format sixels --size " + std::to_string(std::max(4, width)) + "x" +
|
|
|
|
return "chafa --format sixels --size " +
|
|
|
|
|
|
|
|
std::to_string(std::max(4, width)) + "x" +
|
|
|
|
std::to_string(std::max(2, height)) + " " +
|
|
|
|
std::to_string(std::max(2, height)) + " " +
|
|
|
|
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
|
|
|
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (command_exists("img2sixel")) {
|
|
|
|
if (command_exists("img2sixel")) {
|
|
|
|
return "img2sixel -w " + std::to_string(approximate_pixel_width(width)) + "px -h " +
|
|
|
|
return "img2sixel -w " + std::to_string(approximate_pixel_width(width)) +
|
|
|
|
std::to_string(approximate_pixel_height(height)) + "px " +
|
|
|
|
"px -h " + std::to_string(approximate_pixel_height(height)) + "px " +
|
|
|
|
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
|
|
|
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ((forced_protocol == "kitty" || (forced_protocol.empty() && terminal_supports_kitty_graphics())) &&
|
|
|
|
if ((forced_protocol == "kitty" ||
|
|
|
|
|
|
|
|
(forced_protocol.empty() && terminal_supports_kitty_graphics())) &&
|
|
|
|
command_exists("kitten")) {
|
|
|
|
command_exists("kitten")) {
|
|
|
|
TerminalWindowSize size = terminal_window_size();
|
|
|
|
TerminalWindowSize size = terminal_window_size();
|
|
|
|
if (size.cols <= 0) {
|
|
|
|
if (size.cols <= 0) {
|
|
|
|
@@ -578,17 +587,21 @@ std::string App::build_attachment_preview_graphics(const AttachmentInfo& attachm
|
|
|
|
if (size.height_pixels <= 0) {
|
|
|
|
if (size.height_pixels <= 0) {
|
|
|
|
size.height_pixels = std::max(1, size.rows * 16);
|
|
|
|
size.height_pixels = std::max(1, size.rows * 16);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "kitten icat --stdin=no --passthrough=none --transfer-mode=stream --align=left "
|
|
|
|
return "kitten icat --stdin=no --passthrough=none --transfer-mode=stream "
|
|
|
|
"--place " + std::to_string(std::max(1, width)) + "x" + std::to_string(std::max(1, height)) +
|
|
|
|
"--align=left "
|
|
|
|
"@0x0 --use-window-size " + std::to_string(size.cols) + "," + std::to_string(size.rows) + "," +
|
|
|
|
"--place " +
|
|
|
|
std::to_string(size.width_pixels) + "," + std::to_string(size.height_pixels) + " " +
|
|
|
|
std::to_string(std::max(1, width)) + "x" +
|
|
|
|
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
|
|
|
std::to_string(std::max(1, height)) + "@0x0 --use-window-size " +
|
|
|
|
|
|
|
|
std::to_string(size.cols) + "," + std::to_string(size.rows) + "," +
|
|
|
|
|
|
|
|
std::to_string(size.width_pixels) + "," +
|
|
|
|
|
|
|
|
std::to_string(size.height_pixels) + " " + shell_quote(preview_path) +
|
|
|
|
|
|
|
|
" >/dev/tty 2>/dev/null";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (command_exists("chafa")) {
|
|
|
|
if (command_exists("chafa")) {
|
|
|
|
return "chafa --size " + std::to_string(std::max(4, width)) + "x" +
|
|
|
|
return "chafa --size " + std::to_string(std::max(4, width)) + "x" +
|
|
|
|
std::to_string(std::max(2, height)) + " " +
|
|
|
|
std::to_string(std::max(2, height)) + " " + shell_quote(preview_path) +
|
|
|
|
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
|
|
|
" >/dev/tty 2>/dev/null";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
return {};
|
|
|
|
@@ -600,7 +613,8 @@ void App::request_attachment_download(const AttachmentInfo& attachment, bool ope
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pending_attachment_open_ = open_after_download ? std::optional<AttachmentInfo>(attachment) : std::nullopt;
|
|
|
|
pending_attachment_open_ =
|
|
|
|
|
|
|
|
open_after_download ? std::optional<AttachmentInfo>(attachment) : std::nullopt;
|
|
|
|
if (open_after_download) {
|
|
|
|
if (open_after_download) {
|
|
|
|
pending_attachment_download_.reset();
|
|
|
|
pending_attachment_download_.reset();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -639,7 +653,8 @@ void App::open_attachment(const AttachmentInfo& attachment) {
|
|
|
|
if (open_with_system_app(attachment.local_path)) {
|
|
|
|
if (open_with_system_app(attachment.local_path)) {
|
|
|
|
status_line_ = "Video playback failed. Opened in the system app.";
|
|
|
|
status_line_ = "Video playback failed. Opened in the system app.";
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
status_line_ = "No supported video player found. Install `mpv` or `ffplay`.";
|
|
|
|
status_line_ =
|
|
|
|
|
|
|
|
"No supported video player found. Install `mpv` or `ffplay`.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -652,7 +667,8 @@ void App::open_attachment(const AttachmentInfo& attachment) {
|
|
|
|
attachment_viewer_frame_index_ = 0;
|
|
|
|
attachment_viewer_frame_index_ = 0;
|
|
|
|
if (attachment.type == AttachmentType::Video) {
|
|
|
|
if (attachment.type == AttachmentType::Video) {
|
|
|
|
attachment_viewer_animation_frames_ = attachment_animation_frames(attachment);
|
|
|
|
attachment_viewer_animation_frames_ = attachment_animation_frames(attachment);
|
|
|
|
attachment_viewer_next_frame_at_ = std::chrono::steady_clock::now() + std::chrono::milliseconds(125);
|
|
|
|
attachment_viewer_next_frame_at_ =
|
|
|
|
|
|
|
|
std::chrono::steady_clock::now() + std::chrono::milliseconds(125);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!has_inline_attachment_preview(attachment, preview_width, preview_height)) {
|
|
|
|
if (!has_inline_attachment_preview(attachment, preview_width, preview_height)) {
|
|
|
|
attachment_viewer_attachment_.reset();
|
|
|
|
attachment_viewer_attachment_.reset();
|
|
|
|
@@ -660,7 +676,8 @@ void App::open_attachment(const AttachmentInfo& attachment) {
|
|
|
|
attachment_viewer_is_animated_ = false;
|
|
|
|
attachment_viewer_is_animated_ = false;
|
|
|
|
attachment_viewer_frame_index_ = 0;
|
|
|
|
attachment_viewer_frame_index_ = 0;
|
|
|
|
if (open_with_system_app(attachment.local_path)) {
|
|
|
|
if (open_with_system_app(attachment.local_path)) {
|
|
|
|
status_line_ = "Inline preview unavailable. Opened attachment in the system app.";
|
|
|
|
status_line_ =
|
|
|
|
|
|
|
|
"Inline preview unavailable. Opened attachment in the system app.";
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
status_line_ = inline_preview_unavailable_message(attachment);
|
|
|
|
status_line_ = inline_preview_unavailable_message(attachment);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -695,7 +712,8 @@ void App::delete_attachment(const AttachmentInfo& attachment) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (attachment_viewer_attachment_.has_value() && attachment_viewer_attachment_->file_id == attachment.file_id) {
|
|
|
|
if (attachment_viewer_attachment_.has_value() &&
|
|
|
|
|
|
|
|
attachment_viewer_attachment_->file_id == attachment.file_id) {
|
|
|
|
attachment_viewer_open_ = false;
|
|
|
|
attachment_viewer_open_ = false;
|
|
|
|
attachment_viewer_attachment_.reset();
|
|
|
|
attachment_viewer_attachment_.reset();
|
|
|
|
attachment_viewer_animation_frames_.clear();
|
|
|
|
attachment_viewer_animation_frames_.clear();
|
|
|
|
@@ -726,10 +744,9 @@ bool App::export_attachment_to_downloads(const AttachmentInfo& attachment) {
|
|
|
|
const std::filesystem::path target_name = preferred_attachment_filename(attachment);
|
|
|
|
const std::filesystem::path target_name = preferred_attachment_filename(attachment);
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
std::filesystem::create_directories(target_dir);
|
|
|
|
std::filesystem::create_directories(target_dir);
|
|
|
|
const std::filesystem::path target_path = unique_destination_path(target_dir, target_name);
|
|
|
|
const std::filesystem::path target_path =
|
|
|
|
std::filesystem::copy_file(
|
|
|
|
unique_destination_path(target_dir, target_name);
|
|
|
|
attachment.local_path,
|
|
|
|
std::filesystem::copy_file(attachment.local_path, target_path,
|
|
|
|
target_path,
|
|
|
|
|
|
|
|
std::filesystem::copy_options::none);
|
|
|
|
std::filesystem::copy_options::none);
|
|
|
|
status_line_ = "Saved to " + target_path.string();
|
|
|
|
status_line_ = "Saved to " + target_path.string();
|
|
|
|
return true;
|
|
|
|
return true;
|
|
|
|
@@ -783,20 +800,18 @@ void App::refresh_attachment_viewer_content(const AttachmentInfo& attachment) {
|
|
|
|
attachment_viewer_lines_.clear();
|
|
|
|
attachment_viewer_lines_.clear();
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const std::string preview = render_attachment_preview(
|
|
|
|
const std::string preview =
|
|
|
|
attachment,
|
|
|
|
render_attachment_preview(attachment, preview_width, preview_height);
|
|
|
|
preview_width,
|
|
|
|
attachment_viewer_lines_ =
|
|
|
|
preview_height);
|
|
|
|
split_preserved_lines(preview, std::max(10, (COLS > 0 ? COLS : 80) - 8));
|
|
|
|
attachment_viewer_lines_ = split_preserved_lines(
|
|
|
|
|
|
|
|
preview,
|
|
|
|
|
|
|
|
std::max(10, (COLS > 0 ? COLS : 80) - 8));
|
|
|
|
|
|
|
|
if (attachment_viewer_lines_.empty()) {
|
|
|
|
if (attachment_viewer_lines_.empty()) {
|
|
|
|
attachment_viewer_lines_.push_back("Preview is empty.");
|
|
|
|
attachment_viewer_lines_.push_back("Preview is empty.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo &attachment) const {
|
|
|
|
std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo &attachment) const {
|
|
|
|
if (attachment.type != AttachmentType::Video || !attachment.is_downloaded || attachment.local_path.empty()) {
|
|
|
|
if (attachment.type != AttachmentType::Video || !attachment.is_downloaded ||
|
|
|
|
|
|
|
|
attachment.local_path.empty()) {
|
|
|
|
return {};
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!std::filesystem::exists(attachment.local_path) || !command_exists("ffmpeg")) {
|
|
|
|
if (!std::filesystem::exists(attachment.local_path) || !command_exists("ffmpeg")) {
|
|
|
|
@@ -804,7 +819,8 @@ std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo&
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path preview_dir =
|
|
|
|
const std::filesystem::path preview_dir =
|
|
|
|
files_dir_ / "previews" / (std::string("video-") + std::to_string(attachment.file_id));
|
|
|
|
files_dir_ / "previews" /
|
|
|
|
|
|
|
|
(std::string("video-") + std::to_string(attachment.file_id));
|
|
|
|
std::vector<std::string> frames;
|
|
|
|
std::vector<std::string> frames;
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
std::filesystem::create_directories(preview_dir);
|
|
|
|
std::filesystem::create_directories(preview_dir);
|
|
|
|
@@ -857,7 +873,8 @@ std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo&
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool App::advance_attachment_animation() {
|
|
|
|
bool App::advance_attachment_animation() {
|
|
|
|
if (!attachment_viewer_open_ || !attachment_viewer_is_animated_ || attachment_viewer_animation_frames_.empty() ||
|
|
|
|
if (!attachment_viewer_open_ || !attachment_viewer_is_animated_ ||
|
|
|
|
|
|
|
|
attachment_viewer_animation_frames_.empty() ||
|
|
|
|
!attachment_viewer_attachment_.has_value()) {
|
|
|
|
!attachment_viewer_attachment_.has_value()) {
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -881,7 +898,8 @@ std::string App::attachment_preview_path(const AttachmentInfo& attachment) const
|
|
|
|
if (!std::filesystem::exists(attachment.local_path)) {
|
|
|
|
if (!std::filesystem::exists(attachment.local_path)) {
|
|
|
|
return {};
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (attachment_viewer_attachment_.has_value() && attachment_viewer_attachment_->file_id == attachment.file_id &&
|
|
|
|
if (attachment_viewer_attachment_.has_value() &&
|
|
|
|
|
|
|
|
attachment_viewer_attachment_->file_id == attachment.file_id &&
|
|
|
|
!attachment_viewer_animation_frames_.empty() &&
|
|
|
|
!attachment_viewer_animation_frames_.empty() &&
|
|
|
|
attachment_viewer_frame_index_ < attachment_viewer_animation_frames_.size()) {
|
|
|
|
attachment_viewer_frame_index_ < attachment_viewer_animation_frames_.size()) {
|
|
|
|
return attachment_viewer_animation_frames_[attachment_viewer_frame_index_];
|
|
|
|
return attachment_viewer_animation_frames_[attachment_viewer_frame_index_];
|
|
|
|
@@ -894,11 +912,13 @@ std::string App::attachment_preview_path(const AttachmentInfo& attachment) const
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path preview_dir = files_dir_ / "previews";
|
|
|
|
const std::filesystem::path preview_dir = files_dir_ / "previews";
|
|
|
|
const std::filesystem::path preview_path = preview_dir / (std::to_string(attachment.file_id) + ".jpg");
|
|
|
|
const std::filesystem::path preview_path =
|
|
|
|
|
|
|
|
preview_dir / (std::to_string(attachment.file_id) + ".jpg");
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
std::filesystem::create_directories(preview_dir);
|
|
|
|
std::filesystem::create_directories(preview_dir);
|
|
|
|
if (std::filesystem::exists(preview_path)) {
|
|
|
|
if (std::filesystem::exists(preview_path)) {
|
|
|
|
const auto source_time = std::filesystem::last_write_time(attachment.local_path);
|
|
|
|
const auto source_time =
|
|
|
|
|
|
|
|
std::filesystem::last_write_time(attachment.local_path);
|
|
|
|
const auto preview_time = std::filesystem::last_write_time(preview_path);
|
|
|
|
const auto preview_time = std::filesystem::last_write_time(preview_path);
|
|
|
|
if (preview_time >= source_time) {
|
|
|
|
if (preview_time >= source_time) {
|
|
|
|
return preview_path.string();
|
|
|
|
return preview_path.string();
|
|
|
|
@@ -914,7 +934,8 @@ std::string App::attachment_preview_path(const AttachmentInfo& attachment) const
|
|
|
|
shell_quote(preview_path.string()) + " -s 0 -q 8 >/dev/null 2>&1";
|
|
|
|
shell_quote(preview_path.string()) + " -s 0 -q 8 >/dev/null 2>&1";
|
|
|
|
} else if (command_exists("ffmpeg")) {
|
|
|
|
} else if (command_exists("ffmpeg")) {
|
|
|
|
command = "ffmpeg -y -ss 1 -i " + shell_quote(attachment.local_path) +
|
|
|
|
command = "ffmpeg -y -ss 1 -i " + shell_quote(attachment.local_path) +
|
|
|
|
" -frames:v 1 -vf thumbnail " + shell_quote(preview_path.string()) + " >/dev/null 2>&1";
|
|
|
|
" -frames:v 1 -vf thumbnail " + shell_quote(preview_path.string()) +
|
|
|
|
|
|
|
|
" >/dev/null 2>&1";
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
return {};
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -925,11 +946,13 @@ std::string App::attachment_preview_path(const AttachmentInfo& attachment) const
|
|
|
|
return preview_path.string();
|
|
|
|
return preview_path.string();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool App::has_inline_attachment_preview(const AttachmentInfo& attachment, int width, int height) const {
|
|
|
|
bool App::has_inline_attachment_preview(const AttachmentInfo &attachment, int width,
|
|
|
|
|
|
|
|
int height) const {
|
|
|
|
if (attachment_preview_path(attachment).empty()) {
|
|
|
|
if (attachment_preview_path(attachment).empty()) {
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return command_exists("chafa") || !build_attachment_preview_graphics(attachment, width, height).empty();
|
|
|
|
return command_exists("chafa") ||
|
|
|
|
|
|
|
|
!build_attachment_preview_graphics(attachment, width, height).empty();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void App::clear_attachment_preview_graphics() {
|
|
|
|
void App::clear_attachment_preview_graphics() {
|
|
|
|
@@ -963,20 +986,21 @@ void App::render_attachment_preview_graphics(int top, int left, int width, int h
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const std::string forced_protocol = configured_image_protocol();
|
|
|
|
const std::string forced_protocol = configured_image_protocol();
|
|
|
|
const bool want_kitty =
|
|
|
|
const bool want_kitty = forced_protocol == "kitty" ||
|
|
|
|
forced_protocol == "kitty" || (forced_protocol.empty() && terminal_supports_kitty_graphics());
|
|
|
|
(forced_protocol.empty() && terminal_supports_kitty_graphics());
|
|
|
|
const std::string signature =
|
|
|
|
const std::string signature = preview_path + "|" + std::to_string(width) + "x" +
|
|
|
|
preview_path + "|" + std::to_string(width) + "x" + std::to_string(height) + "|" +
|
|
|
|
std::to_string(height) + "|" + std::to_string(top) + ":" +
|
|
|
|
std::to_string(top) + ":" + std::to_string(left) + "|" + forced_protocol;
|
|
|
|
std::to_string(left) + "|" + forced_protocol;
|
|
|
|
const bool want_sixels =
|
|
|
|
const bool want_sixels = forced_protocol == "sixels" ||
|
|
|
|
forced_protocol == "sixels" || (forced_protocol.empty() && terminal_supports_sixels());
|
|
|
|
(forced_protocol.empty() && terminal_supports_sixels());
|
|
|
|
const std::string command = build_attachment_preview_graphics(attachment, width, height);
|
|
|
|
const std::string command = build_attachment_preview_graphics(attachment, width, height);
|
|
|
|
if (command.empty()) {
|
|
|
|
if (command.empty()) {
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const bool same_signature = attachment_preview_signature_ == signature && attachment_preview_graphics_visible_;
|
|
|
|
const bool same_signature =
|
|
|
|
|
|
|
|
attachment_preview_signature_ == signature && attachment_preview_graphics_visible_;
|
|
|
|
if (!same_signature) {
|
|
|
|
if (!same_signature) {
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -997,18 +1021,23 @@ void App::render_attachment_preview_graphics(int top, int left, int width, int h
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::string kitty_command =
|
|
|
|
const std::string kitty_command =
|
|
|
|
"kitten icat --stdin=no --passthrough=none --transfer-mode=stream --align=left "
|
|
|
|
"kitten icat --stdin=no --passthrough=none --transfer-mode=stream "
|
|
|
|
"--place " + std::to_string(std::max(1, width)) + "x" + std::to_string(std::max(1, height)) +
|
|
|
|
"--align=left "
|
|
|
|
"@" + std::to_string(std::max(0, left)) + "x" + std::to_string(std::max(0, top)) +
|
|
|
|
"--place " +
|
|
|
|
" --use-window-size " + std::to_string(size.cols) + "," + std::to_string(size.rows) + "," +
|
|
|
|
std::to_string(std::max(1, width)) + "x" +
|
|
|
|
std::to_string(size.width_pixels) + "," + std::to_string(size.height_pixels) +
|
|
|
|
std::to_string(std::max(1, height)) + "@" +
|
|
|
|
" " + shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
|
|
|
std::to_string(std::max(0, left)) + "x" + std::to_string(std::max(0, top)) +
|
|
|
|
|
|
|
|
" --use-window-size " + std::to_string(size.cols) + "," +
|
|
|
|
|
|
|
|
std::to_string(size.rows) + "," + std::to_string(size.width_pixels) + "," +
|
|
|
|
|
|
|
|
std::to_string(size.height_pixels) + " " + shell_quote(preview_path) +
|
|
|
|
|
|
|
|
" >/dev/tty 2>/dev/null";
|
|
|
|
if (run_command_to_terminal(kitty_command) != 0) {
|
|
|
|
if (run_command_to_terminal(kitty_command) != 0) {
|
|
|
|
attachment_preview_graphics_is_sixel_ = false;
|
|
|
|
attachment_preview_graphics_is_sixel_ = false;
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
write_terminal_output("\0337\033[" + std::to_string(top + 1) + ";" + std::to_string(left + 1) + "H");
|
|
|
|
write_terminal_output("\0337\033[" + std::to_string(top + 1) + ";" +
|
|
|
|
|
|
|
|
std::to_string(left + 1) + "H");
|
|
|
|
if (run_command_to_terminal(command) != 0) {
|
|
|
|
if (run_command_to_terminal(command) != 0) {
|
|
|
|
write_terminal_output("\0338");
|
|
|
|
write_terminal_output("\0338");
|
|
|
|
attachment_preview_graphics_is_sixel_ = false;
|
|
|
|
attachment_preview_graphics_is_sixel_ = false;
|
|
|
|
@@ -1046,14 +1075,14 @@ void App::handle_attachments_menu_key(int ch) {
|
|
|
|
status_line_ = "Attachment actions.";
|
|
|
|
status_line_ = "Attachment actions.";
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
case KEY_LEFT:
|
|
|
|
case KEY_LEFT:
|
|
|
|
attachment_category_index_ =
|
|
|
|
attachment_category_index_ = (attachment_category_index_ +
|
|
|
|
(attachment_category_index_ + static_cast<int>(kAttachmentTypes.size()) - 1) %
|
|
|
|
static_cast<int>(kAttachmentTypes.size()) - 1) %
|
|
|
|
static_cast<int>(kAttachmentTypes.size());
|
|
|
|
static_cast<int>(kAttachmentTypes.size());
|
|
|
|
attachment_selection_index_ = 0;
|
|
|
|
attachment_selection_index_ = 0;
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
case KEY_RIGHT:
|
|
|
|
case KEY_RIGHT:
|
|
|
|
attachment_category_index_ =
|
|
|
|
attachment_category_index_ = (attachment_category_index_ + 1) %
|
|
|
|
(attachment_category_index_ + 1) % static_cast<int>(kAttachmentTypes.size());
|
|
|
|
static_cast<int>(kAttachmentTypes.size());
|
|
|
|
attachment_selection_index_ = 0;
|
|
|
|
attachment_selection_index_ = 0;
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
case KEY_UP:
|
|
|
|
case KEY_UP:
|
|
|
|
@@ -1141,9 +1170,11 @@ void App::handle_attachment_viewer_key(int ch) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
{
|
|
|
|
{
|
|
|
|
const std::string local_path = attachment_viewer_attachment_->local_path;
|
|
|
|
const std::string local_path =
|
|
|
|
|
|
|
|
attachment_viewer_attachment_->local_path;
|
|
|
|
const std::string caption = attachment_viewer_send_caption_;
|
|
|
|
const std::string caption = attachment_viewer_send_caption_;
|
|
|
|
const auto reply_to_message_id = attachment_viewer_send_reply_to_message_id_;
|
|
|
|
const auto reply_to_message_id =
|
|
|
|
|
|
|
|
attachment_viewer_send_reply_to_message_id_;
|
|
|
|
const std::int64_t chat_id = attachment_viewer_send_chat_id_;
|
|
|
|
const std::int64_t chat_id = attachment_viewer_send_chat_id_;
|
|
|
|
attachment_viewer_open_ = false;
|
|
|
|
attachment_viewer_open_ = false;
|
|
|
|
attachment_viewer_attachment_.reset();
|
|
|
|
attachment_viewer_attachment_.reset();
|
|
|
|
@@ -1152,7 +1183,8 @@ void App::handle_attachment_viewer_key(int ch) {
|
|
|
|
attachment_viewer_frame_index_ = 0;
|
|
|
|
attachment_viewer_frame_index_ = 0;
|
|
|
|
reset_attachment_viewer_send_preview();
|
|
|
|
reset_attachment_viewer_send_preview();
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
send_photo_message(chat_id, local_path, caption, reply_to_message_id);
|
|
|
|
send_photo_message(chat_id, local_path, caption,
|
|
|
|
|
|
|
|
reply_to_message_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
default:
|
|
|
|
default:
|
|
|
|
@@ -1163,8 +1195,7 @@ void App::handle_attachment_viewer_key(int ch) {
|
|
|
|
switch (ch) {
|
|
|
|
switch (ch) {
|
|
|
|
case 27:
|
|
|
|
case 27:
|
|
|
|
case 'q':
|
|
|
|
case 'q':
|
|
|
|
case ' ':
|
|
|
|
case ' ': {
|
|
|
|
{
|
|
|
|
|
|
|
|
const bool was_send_preview = attachment_viewer_send_on_enter_;
|
|
|
|
const bool was_send_preview = attachment_viewer_send_on_enter_;
|
|
|
|
attachment_viewer_open_ = false;
|
|
|
|
attachment_viewer_open_ = false;
|
|
|
|
attachment_viewer_attachment_.reset();
|
|
|
|
attachment_viewer_attachment_.reset();
|
|
|
|
@@ -1173,8 +1204,7 @@ void App::handle_attachment_viewer_key(int ch) {
|
|
|
|
attachment_viewer_frame_index_ = 0;
|
|
|
|
attachment_viewer_frame_index_ = 0;
|
|
|
|
reset_attachment_viewer_send_preview();
|
|
|
|
reset_attachment_viewer_send_preview();
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
status_line_ = was_send_preview
|
|
|
|
status_line_ = was_send_preview ? "Cancelled clipboard image send."
|
|
|
|
? "Cancelled clipboard image send."
|
|
|
|
|
|
|
|
: "Closed attachment preview.";
|
|
|
|
: "Closed attachment preview.";
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1232,7 +1262,8 @@ void App::draw_attachments_menu(int height, int width) {
|
|
|
|
|
|
|
|
|
|
|
|
const AttachmentType selected_type = kAttachmentTypes[attachment_category_index_];
|
|
|
|
const AttachmentType selected_type = kAttachmentTypes[attachment_category_index_];
|
|
|
|
std::vector<const MessageInfo *> attachments;
|
|
|
|
std::vector<const MessageInfo *> attachments;
|
|
|
|
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend(); ++it) {
|
|
|
|
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend();
|
|
|
|
|
|
|
|
++it) {
|
|
|
|
if (it->has_attachment && it->attachment.type == selected_type) {
|
|
|
|
if (it->has_attachment && it->attachment.type == selected_type) {
|
|
|
|
attachments.push_back(&*it);
|
|
|
|
attachments.push_back(&*it);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1269,7 +1300,8 @@ void App::draw_attachments_menu(int height, int width) {
|
|
|
|
int date_width = std::min(16, std::max(8, content_width / 3));
|
|
|
|
int date_width = std::min(16, std::max(8, content_width / 3));
|
|
|
|
int sender_width = std::max(8, content_width - status_width - size_width - date_width - 3);
|
|
|
|
int sender_width = std::max(8, content_width - status_width - size_width - date_width - 3);
|
|
|
|
if (sender_width + status_width + size_width + date_width + 3 > content_width) {
|
|
|
|
if (sender_width + status_width + size_width + date_width + 3 > content_width) {
|
|
|
|
date_width = std::max(8, content_width - sender_width - status_width - size_width - 3);
|
|
|
|
date_width =
|
|
|
|
|
|
|
|
std::max(8, content_width - sender_width - status_width - size_width - 3);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mvwaddnstr(window, 3, 2, "Sender", sender_width);
|
|
|
|
mvwaddnstr(window, 3, 2, "Sender", sender_width);
|
|
|
|
@@ -1286,7 +1318,8 @@ void App::draw_attachments_menu(int height, int width) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (attachments.empty()) {
|
|
|
|
if (attachments.empty()) {
|
|
|
|
const std::string message = "No " + attachment_type_label(selected_type) + " in this chat.";
|
|
|
|
const std::string message =
|
|
|
|
|
|
|
|
"No " + attachment_type_label(selected_type) + " in this chat.";
|
|
|
|
mvwaddnstr(window, list_top, 2, message.c_str(), menu_width - 4);
|
|
|
|
mvwaddnstr(window, list_top, 2, message.c_str(), menu_width - 4);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
for (int row = 0; row < list_height; ++row) {
|
|
|
|
for (int row = 0; row < list_height; ++row) {
|
|
|
|
@@ -1297,24 +1330,29 @@ void App::draw_attachments_menu(int height, int width) {
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const MessageInfo& message = *attachments[static_cast<std::size_t>(item_index)];
|
|
|
|
const MessageInfo &message =
|
|
|
|
|
|
|
|
*attachments[static_cast<std::size_t>(item_index)];
|
|
|
|
if (item_index == attachment_selection_index_) {
|
|
|
|
if (item_index == attachment_selection_index_) {
|
|
|
|
wattron(window, A_REVERSE);
|
|
|
|
wattron(window, A_REVERSE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const std::string sender = truncate_text(message.sender, sender_width);
|
|
|
|
const std::string sender = truncate_text(message.sender, sender_width);
|
|
|
|
const std::string dl_status = truncate_text(
|
|
|
|
const std::string dl_status = truncate_text(
|
|
|
|
message.attachment.is_downloading_active && !message.attachment.is_downloaded
|
|
|
|
message.attachment.is_downloading_active &&
|
|
|
|
? attachment_progress_bar(message.attachment, std::max(10, status_width - 2))
|
|
|
|
!message.attachment.is_downloaded
|
|
|
|
|
|
|
|
? attachment_progress_bar(message.attachment,
|
|
|
|
|
|
|
|
std::max(10, status_width - 2))
|
|
|
|
: (message.attachment.is_downloaded ? "Ready" : ""),
|
|
|
|
: (message.attachment.is_downloaded ? "Ready" : ""),
|
|
|
|
status_width);
|
|
|
|
status_width);
|
|
|
|
const std::string file_size = truncate_text(
|
|
|
|
const std::string file_size = truncate_text(
|
|
|
|
format_file_size(message.attachment.size_bytes),
|
|
|
|
format_file_size(message.attachment.size_bytes), size_width);
|
|
|
|
size_width);
|
|
|
|
const std::string sent =
|
|
|
|
const std::string sent = truncate_text(format_datetime(message.date), date_width);
|
|
|
|
truncate_text(format_datetime(message.date), date_width);
|
|
|
|
mvwaddnstr(window, y, 2, sender.c_str(), sender_width);
|
|
|
|
mvwaddnstr(window, y, 2, sender.c_str(), sender_width);
|
|
|
|
mvwaddnstr(window, y, 3 + sender_width, dl_status.c_str(), status_width);
|
|
|
|
mvwaddnstr(window, y, 3 + sender_width, dl_status.c_str(), status_width);
|
|
|
|
mvwaddnstr(window, y, 4 + sender_width + status_width, file_size.c_str(), size_width);
|
|
|
|
mvwaddnstr(window, y, 4 + sender_width + status_width, file_size.c_str(),
|
|
|
|
mvwaddnstr(window, y, 5 + sender_width + status_width + size_width, sent.c_str(), date_width);
|
|
|
|
size_width);
|
|
|
|
|
|
|
|
mvwaddnstr(window, y, 5 + sender_width + status_width + size_width,
|
|
|
|
|
|
|
|
sent.c_str(), date_width);
|
|
|
|
if (item_index == attachment_selection_index_) {
|
|
|
|
if (item_index == attachment_selection_index_) {
|
|
|
|
wattroff(window, A_REVERSE);
|
|
|
|
wattroff(window, A_REVERSE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1325,7 +1363,8 @@ void App::draw_attachments_menu(int height, int width) {
|
|
|
|
std::string title_line;
|
|
|
|
std::string title_line;
|
|
|
|
std::string actions_line = "Left/Right type Up/Down move Space actions Esc close";
|
|
|
|
std::string actions_line = "Left/Right type Up/Down move Space actions Esc close";
|
|
|
|
if (!attachments.empty()) {
|
|
|
|
if (!attachments.empty()) {
|
|
|
|
const MessageInfo& selected = *attachments[static_cast<std::size_t>(attachment_selection_index_)];
|
|
|
|
const MessageInfo &selected =
|
|
|
|
|
|
|
|
*attachments[static_cast<std::size_t>(attachment_selection_index_)];
|
|
|
|
title_line = truncate_text(selected.attachment.name, menu_width - 4);
|
|
|
|
title_line = truncate_text(selected.attachment.name, menu_width - 4);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
title_line = truncate_text("No attachment selected.", menu_width - 4);
|
|
|
|
title_line = truncate_text("No attachment selected.", menu_width - 4);
|
|
|
|
@@ -1357,14 +1396,16 @@ void App::draw_attachment_action_menu(int height, int width) {
|
|
|
|
|
|
|
|
|
|
|
|
box(window, 0, 0);
|
|
|
|
box(window, 0, 0);
|
|
|
|
mvwprintw(window, 0, 2, " Attachment Action ");
|
|
|
|
mvwprintw(window, 0, 2, " Attachment Action ");
|
|
|
|
mvwaddnstr(window, 1, 2, truncate_text(attachment->name, menu_width - 4).c_str(), menu_width - 4);
|
|
|
|
mvwaddnstr(window, 1, 2, truncate_text(attachment->name, menu_width - 4).c_str(),
|
|
|
|
|
|
|
|
menu_width - 4);
|
|
|
|
mvwhline(window, 3, 1, ACS_HLINE, menu_width - 2);
|
|
|
|
mvwhline(window, 3, 1, ACS_HLINE, menu_width - 2);
|
|
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> prompt_lines = wrap_text(
|
|
|
|
const std::vector<std::string> prompt_lines =
|
|
|
|
"What do you want to do with this file?",
|
|
|
|
wrap_text("What do you want to do with this file?", std::max(10, menu_width - 8));
|
|
|
|
std::max(10, menu_width - 8));
|
|
|
|
for (std::size_t i = 0; i < prompt_lines.size() && static_cast<int>(i) < menu_height - 9;
|
|
|
|
for (std::size_t i = 0; i < prompt_lines.size() && static_cast<int>(i) < menu_height - 9; ++i) {
|
|
|
|
++i) {
|
|
|
|
mvwaddnstr(window, 5 + static_cast<int>(i), 4, prompt_lines[i].c_str(), menu_width - 8);
|
|
|
|
mvwaddnstr(window, 5 + static_cast<int>(i), 4, prompt_lines[i].c_str(),
|
|
|
|
|
|
|
|
menu_width - 8);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mvwhline(window, menu_height - 5, 1, ACS_HLINE, menu_width - 2);
|
|
|
|
mvwhline(window, menu_height - 5, 1, ACS_HLINE, menu_width - 2);
|
|
|
|
@@ -1378,20 +1419,23 @@ void App::draw_attachment_action_menu(int height, int width) {
|
|
|
|
|
|
|
|
|
|
|
|
const int button_y = menu_height - 3;
|
|
|
|
const int button_y = menu_height - 3;
|
|
|
|
const int inner_width = menu_width - 2;
|
|
|
|
const int inner_width = menu_width - 2;
|
|
|
|
const int gap = std::max(2, (inner_width - total_label_width) / (static_cast<int>(labels.size()) + 1));
|
|
|
|
const int gap = std::max(2, (inner_width - total_label_width) /
|
|
|
|
|
|
|
|
(static_cast<int>(labels.size()) + 1));
|
|
|
|
int button_x = 1 + gap;
|
|
|
|
int button_x = 1 + gap;
|
|
|
|
for (std::size_t i = 0; i < actions.size(); ++i) {
|
|
|
|
for (std::size_t i = 0; i < actions.size(); ++i) {
|
|
|
|
if (static_cast<int>(i) == attachment_action_index_) {
|
|
|
|
if (static_cast<int>(i) == attachment_action_index_) {
|
|
|
|
wattron(window, A_REVERSE | A_BOLD);
|
|
|
|
wattron(window, A_REVERSE | A_BOLD);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mvwaddnstr(window, button_y, button_x, labels[i].c_str(), menu_width - button_x - 2);
|
|
|
|
mvwaddnstr(window, button_y, button_x, labels[i].c_str(),
|
|
|
|
|
|
|
|
menu_width - button_x - 2);
|
|
|
|
if (static_cast<int>(i) == attachment_action_index_) {
|
|
|
|
if (static_cast<int>(i) == attachment_action_index_) {
|
|
|
|
wattroff(window, A_REVERSE | A_BOLD);
|
|
|
|
wattroff(window, A_REVERSE | A_BOLD);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
button_x += static_cast<int>(labels[i].size()) + gap;
|
|
|
|
button_x += static_cast<int>(labels[i].size()) + gap;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mvwaddnstr(window, menu_height - 2, 2, "Left/Right move Enter select Esc close", menu_width - 4);
|
|
|
|
mvwaddnstr(window, menu_height - 2, 2, "Left/Right move Enter select Esc close",
|
|
|
|
|
|
|
|
menu_width - 4);
|
|
|
|
wrefresh(window);
|
|
|
|
wrefresh(window);
|
|
|
|
delwin(window);
|
|
|
|
delwin(window);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1414,7 +1458,8 @@ void App::draw_attachment_viewer(int height, int width) {
|
|
|
|
|
|
|
|
|
|
|
|
const int content_top = 3;
|
|
|
|
const int content_top = 3;
|
|
|
|
const int content_height = std::max(1, menu_height - 5);
|
|
|
|
const int content_height = std::max(1, menu_height - 5);
|
|
|
|
const int max_scroll = std::max(0, static_cast<int>(attachment_viewer_lines_.size()) - content_height);
|
|
|
|
const int max_scroll =
|
|
|
|
|
|
|
|
std::max(0, static_cast<int>(attachment_viewer_lines_.size()) - content_height);
|
|
|
|
if (attachment_viewer_scroll_ > max_scroll) {
|
|
|
|
if (attachment_viewer_scroll_ > max_scroll) {
|
|
|
|
attachment_viewer_scroll_ = max_scroll;
|
|
|
|
attachment_viewer_scroll_ = max_scroll;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1425,10 +1470,7 @@ void App::draw_attachment_viewer(int height, int width) {
|
|
|
|
if (line_index >= static_cast<int>(attachment_viewer_lines_.size())) {
|
|
|
|
if (line_index >= static_cast<int>(attachment_viewer_lines_.size())) {
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mvwaddnstr(
|
|
|
|
mvwaddnstr(window, content_top + row, 2,
|
|
|
|
window,
|
|
|
|
|
|
|
|
content_top + row,
|
|
|
|
|
|
|
|
2,
|
|
|
|
|
|
|
|
attachment_viewer_lines_[static_cast<std::size_t>(line_index)].c_str(),
|
|
|
|
attachment_viewer_lines_[static_cast<std::size_t>(line_index)].c_str(),
|
|
|
|
menu_width - 4);
|
|
|
|
menu_width - 4);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1439,7 +1481,8 @@ void App::draw_attachment_viewer(int height, int width) {
|
|
|
|
mvwaddnstr(window, menu_height - 2, 2, controls.c_str(), menu_width - 4);
|
|
|
|
mvwaddnstr(window, menu_height - 2, 2, controls.c_str(), menu_width - 4);
|
|
|
|
wrefresh(window);
|
|
|
|
wrefresh(window);
|
|
|
|
if (attachment_viewer_attachment_.has_value()) {
|
|
|
|
if (attachment_viewer_attachment_.has_value()) {
|
|
|
|
render_attachment_preview_graphics(top + content_top, left + 2, menu_width - 4, content_height);
|
|
|
|
render_attachment_preview_graphics(top + content_top, left + 2, menu_width - 4,
|
|
|
|
|
|
|
|
content_height);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
clear_attachment_preview_graphics();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|