/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/ringtones_box.h" #include "api/api_ringtones.h" #include "apiwrap.h" #include "base/base_file_utilities.h" #include "base/call_delayed.h" #include "base/event_filter.h" #include "base/timer_rpl.h" #include "base/unixtime.h" #include "core/application.h" #include "core/core_settings.h" #include "core/file_utilities.h" #include "core/mime_type.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_document_resolver.h" #include "data/data_thread.h" #include "data/data_session.h" #include "data/notify/data_notify_settings.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "media/audio/media_audio.h" #include "settings/settings_common.h" #include "ui/boxes/confirm_box.h" #include "ui/text/format_values.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/vertical_layout.h" #include "styles/style_menu_icons.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" #include "styles/style_settings.h" namespace { constexpr auto kDefaultValue = -1; constexpr auto kNoSoundValue = -2; constexpr auto kNoDetachTimeout = crl::time(250); class AudioCreator final { public: AudioCreator(); AudioCreator(AudioCreator &&other); ~AudioCreator(); private: rpl::lifetime _lifetime; bool _attached = false; }; AudioCreator::AudioCreator() : _attached(true) { crl::async([] { QMutexLocker lock(Media::Player::internal::audioPlayerMutex()); Media::Audio::AttachToDevice(); }); base::timer_each( kNoDetachTimeout ) | rpl::start_with_next([=] { Media::Audio::StopDetachIfNotUsedSafe(); }, _lifetime); } AudioCreator::AudioCreator(AudioCreator &&other) : _lifetime(base::take(other._lifetime)) , _attached(base::take(other._attached)) { } AudioCreator::~AudioCreator() { if (_attached) { Media::Audio::ScheduleDetachIfNotUsedSafe(); } } } // namespace QString ExtractRingtoneName(not_null document) { if (document->isNull()) { return QString(); } const auto name = document->filename(); if (!name.isEmpty()) { const auto extension = Data::FileExtension(name); if (extension.isEmpty()) { return name; } else if (name.size() > extension.size() + 1) { return name.mid(0, name.size() - extension.size() - 1); } } const auto date = langDateTime( base::unixtime::parse(document->date)); const auto base = document->isVoiceMessage() ? (tr::lng_in_dlg_audio(tr::now) + ' ') : document->isAudioFile() ? (tr::lng_in_dlg_audio_file(tr::now) + ' ') : QString(); return base + date; } void RingtonesBox( not_null box, not_null session, Data::NotifySound selected, Fn save) { box->setTitle(tr::lng_ringtones_box_title()); const auto container = box->verticalLayout(); auto padding = st::boxPadding; padding.setTop(padding.bottom()); struct State { AudioCreator creator; std::shared_ptr group; std::vector> medias; Data::NotifySound chosen; base::unique_qptr menu; QPointer defaultButton; QPointer chosenButton; std::vector> buttons; }; const auto state = container->lifetime().make_state(State{ .group = std::make_shared(), .chosen = selected, }); const auto addToGroup = [=]( not_null verticalLayout, int value, const QString &text, bool chosen) { if (chosen) { state->group->setValue(value); } const auto button = verticalLayout->add( object_ptr( verticalLayout, state->group, value, text, st::defaultCheckbox), padding); if (chosen) { state->chosenButton = button; } if (value == kDefaultValue) { state->defaultButton = button; button->setClickedCallback([=] { Core::App().notifications().playSound(session, 0); }); } if (value < 0) { return; } while (state->buttons.size() <= value) { state->buttons.push_back(nullptr); } button->setClickedCallback([=] { const auto media = state->medias[value].get(); if (media->loaded()) { Core::App().notifications().playSound( session, media->owner()->id); } }); base::install_event_filter(button, [=](not_null e) { if (e->type() != QEvent::ContextMenu || state->menu) { return base::EventFilterResult::Continue; } state->menu = base::make_unique_q( button, st::popupMenuWithIcons); auto callback = [=] { const auto id = state->medias[value]->owner()->id; session->api().ringtones().remove(id); }; state->menu->addAction( tr::lng_box_delete(tr::now), std::move(callback), &st::menuIconDelete); state->menu->popup(QCursor::pos()); return base::EventFilterResult::Cancel; }); }; session->api().ringtones().uploadFails( ) | rpl::start_with_next([=](const QString &error) { if ((error == u"RINGTONE_DURATION_TOO_LONG"_q)) { box->getDelegate()->show(Ui::MakeInformBox( tr::lng_ringtones_error_max_duration( tr::now, lt_duration, Ui::FormatMuteFor( session->api().ringtones().maxDuration())))); } else if ((error == u"RINGTONE_SIZE_TOO_BIG"_q)) { box->getDelegate()->show(Ui::MakeInformBox( tr::lng_ringtones_error_max_size( tr::now, lt_size, Ui::FormatSizeText( session->api().ringtones().maxSize())))); } else if (error == u"RINGTONE_MIME_INVALID"_q) { box->getDelegate()->show( Ui::MakeInformBox(tr::lng_edit_media_invalid_file())); } }, box->lifetime()); Settings::AddSubsectionTitle( container, tr::lng_ringtones_box_cloud_subtitle()); const auto noSound = selected.none; addToGroup( container, kDefaultValue, tr::lng_ringtones_box_default(tr::now), false); addToGroup( container, kNoSoundValue, tr::lng_ringtones_box_no_sound(tr::now), noSound); const auto custom = container->add( object_ptr(container)); const auto rebuild = [=] { const auto old = base::take(state->medias); auto value = 0; while (custom->count()) { delete custom->widgetAt(0); } for (const auto &id : session->api().ringtones().list()) { const auto chosen = (state->chosen.id && state->chosen.id == id); const auto document = session->data().document(id); const auto text = ExtractRingtoneName(document); addToGroup(custom, value++, text, chosen); state->medias.push_back(document->createMediaView()); document->owner().notifySettings().cacheSound(document); } custom->resizeToWidth(container->width()); if (!state->chosenButton) { state->group->setValue(kDefaultValue); state->defaultButton->finishAnimating(); } }; session->api().ringtones().listUpdates( ) | rpl::start_with_next(rebuild, container->lifetime()); session->api().ringtones().uploadDones( ) | rpl::start_with_next([=](DocumentId id) { state->chosen = Data::NotifySound{ .id = id }; rebuild(); }, container->lifetime()); session->api().ringtones().requestList(); rebuild(); const auto upload = box->addRow( Settings::CreateButton( container, tr::lng_ringtones_box_upload_button(), st::ringtonesBoxButton, { &st::settingsIconAdd, Settings::IconType::Round, &st::windowBgActive }), style::margins()); upload->addClickHandler([=] { const auto delay = st::ringtonesBoxButton.ripple.hideDuration; base::call_delayed(delay, crl::guard(box, [=] { const auto callback = [=](const FileDialog::OpenResult &result) { auto mime = QString(); auto name = QString(); auto content = result.remoteContent; if (!result.paths.isEmpty()) { auto info = QFileInfo(result.paths.front()); mime = Core::MimeTypeForFile(info).name(); name = info.fileName(); auto f = QFile(result.paths.front()); if (f.open(QIODevice::ReadOnly)) { content = f.readAll(); f.close(); } } else { mime = Core::MimeTypeForData(content).name(); name = "audio"; } const auto &ringtones = session->api().ringtones(); if (int(content.size()) > ringtones.maxSize()) { box->getDelegate()->show(Ui::MakeInformBox( tr::lng_ringtones_error_max_size( tr::now, lt_size, Ui::FormatSizeText(ringtones.maxSize())))); return; } session->api().ringtones().upload(name, mime, content); }; FileDialog::GetOpenPath( box.get(), tr::lng_ringtones_box_upload_choose(tr::now), "Audio files (*.mp3)", crl::guard(box, callback)); })); }); box->addSkip(st::ringtonesBoxSkip); Settings::AddDividerText(container, tr::lng_ringtones_box_about()); box->addSkip(st::ringtonesBoxSkip); box->setWidth(st::boxWideWidth); box->addButton(tr::lng_settings_save(), [=] { const auto value = state->group->value(); auto sound = (value == kDefaultValue) ? Data::NotifySound() : (value == kNoSoundValue) ? Data::NotifySound{ .none = true } : Data::NotifySound{ .id = state->medias[value]->owner()->id }; save(sound); box->closeBox(); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } void ThreadRingtonesBox( not_null box, not_null thread) { const auto now = thread->owner().notifySettings().sound(thread); RingtonesBox(box, &thread->session(), now, [=](Data::NotifySound sound) { thread->owner().notifySettings().update(thread, {}, {}, sound); }); }