352 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| 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_peer.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<DocumentData*> 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<Ui::GenericBox*> box,
 | |
| 		not_null<Main::Session*> session,
 | |
| 		Data::NotifySound selected,
 | |
| 		Fn<void(Data::NotifySound)> 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<Ui::RadiobuttonGroup> group;
 | |
| 		std::vector<std::shared_ptr<Data::DocumentMedia>> medias;
 | |
| 		Data::NotifySound chosen;
 | |
| 		base::unique_qptr<Ui::PopupMenu> menu;
 | |
| 		QPointer<Ui::Radiobutton> defaultButton;
 | |
| 		QPointer<Ui::Radiobutton> chosenButton;
 | |
| 		std::vector<QPointer<Ui::Radiobutton>> buttons;
 | |
| 	};
 | |
| 	const auto state = container->lifetime().make_state<State>(State{
 | |
| 		.group = std::make_shared<Ui::RadiobuttonGroup>(),
 | |
| 		.chosen = selected,
 | |
| 	});
 | |
| 
 | |
| 	const auto addToGroup = [=](
 | |
| 			not_null<Ui::VerticalLayout*> verticalLayout,
 | |
| 			int value,
 | |
| 			const QString &text,
 | |
| 			bool chosen) {
 | |
| 		if (chosen) {
 | |
| 			state->group->setValue(value);
 | |
| 		}
 | |
| 		const auto button = verticalLayout->add(
 | |
| 			object_ptr<Ui::Radiobutton>(
 | |
| 				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<QEvent*> e) {
 | |
| 			if (e->type() != QEvent::ContextMenu || state->menu) {
 | |
| 				return base::EventFilterResult::Continue;
 | |
| 			}
 | |
| 			state->menu = base::make_unique_q<Ui::PopupMenu>(
 | |
| 				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<Ui::VerticalLayout>(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,
 | |
| 				0,
 | |
| 				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 PeerRingtonesBox(
 | |
| 		not_null<Ui::GenericBox*> box,
 | |
| 		not_null<PeerData*> peer) {
 | |
| 	const auto now = peer->owner().notifySettings().sound(peer);
 | |
| 	RingtonesBox(box, &peer->session(), now, [=](Data::NotifySound sound) {
 | |
| 		peer->owner().notifySettings().update(peer, {}, {}, sound);
 | |
| 	});
 | |
| }
 | 
