ringtones_box.cpp 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "boxes/ringtones_box.h"
  8. #include "api/api_ringtones.h"
  9. #include "apiwrap.h"
  10. #include "base/call_delayed.h"
  11. #include "base/event_filter.h"
  12. #include "base/timer_rpl.h"
  13. #include "base/unixtime.h"
  14. #include "core/application.h"
  15. #include "core/core_settings.h"
  16. #include "core/file_utilities.h"
  17. #include "core/mime_type.h"
  18. #include "data/data_document.h"
  19. #include "data/data_document_media.h"
  20. #include "data/data_document_resolver.h"
  21. #include "data/data_thread.h"
  22. #include "data/data_session.h"
  23. #include "data/notify/data_notify_settings.h"
  24. #include "lang/lang_keys.h"
  25. #include "main/main_session.h"
  26. #include "media/audio/media_audio.h"
  27. #include "platform/platform_notifications_manager.h"
  28. #include "settings/settings_common.h"
  29. #include "ui/boxes/confirm_box.h"
  30. #include "ui/text/format_values.h"
  31. #include "ui/widgets/checkbox.h"
  32. #include "ui/widgets/popup_menu.h"
  33. #include "ui/widgets/scroll_area.h"
  34. #include "ui/wrap/vertical_layout.h"
  35. #include "ui/vertical_list.h"
  36. #include "styles/style_menu_icons.h"
  37. #include "styles/style_boxes.h"
  38. #include "styles/style_layers.h"
  39. #include "styles/style_settings.h"
  40. namespace {
  41. constexpr auto kDefaultValue = -1;
  42. constexpr auto kNoSoundValue = -2;
  43. constexpr auto kNoDetachTimeout = crl::time(250);
  44. class AudioCreator final {
  45. public:
  46. AudioCreator();
  47. AudioCreator(AudioCreator &&other);
  48. ~AudioCreator();
  49. private:
  50. rpl::lifetime _lifetime;
  51. bool _attached = false;
  52. };
  53. AudioCreator::AudioCreator()
  54. : _attached(true) {
  55. crl::async([] {
  56. QMutexLocker lock(Media::Player::internal::audioPlayerMutex());
  57. Media::Audio::AttachToDevice();
  58. });
  59. base::timer_each(
  60. kNoDetachTimeout
  61. ) | rpl::start_with_next([=] {
  62. Media::Audio::StopDetachIfNotUsedSafe();
  63. }, _lifetime);
  64. }
  65. AudioCreator::AudioCreator(AudioCreator &&other)
  66. : _lifetime(base::take(other._lifetime))
  67. , _attached(base::take(other._attached)) {
  68. }
  69. AudioCreator::~AudioCreator() {
  70. if (_attached) {
  71. Media::Audio::ScheduleDetachIfNotUsedSafe();
  72. }
  73. }
  74. } // namespace
  75. QString ExtractRingtoneName(not_null<DocumentData*> document) {
  76. if (document->isNull()) {
  77. return QString();
  78. }
  79. const auto name = document->filename();
  80. if (!name.isEmpty()) {
  81. const auto extension = Core::FileExtension(name);
  82. if (extension.isEmpty()) {
  83. return name;
  84. } else if (name.size() > extension.size() + 1) {
  85. return name.mid(0, name.size() - extension.size() - 1);
  86. }
  87. }
  88. const auto date = langDateTime(
  89. base::unixtime::parse(document->date));
  90. const auto base = document->isVoiceMessage()
  91. ? (tr::lng_in_dlg_audio(tr::now) + ' ')
  92. : document->isAudioFile()
  93. ? (tr::lng_in_dlg_audio_file(tr::now) + ' ')
  94. : QString();
  95. return base + date;
  96. }
  97. void RingtonesBox(
  98. not_null<Ui::GenericBox*> box,
  99. not_null<Main::Session*> session,
  100. Data::NotifySound selected,
  101. Fn<void(Data::NotifySound)> save) {
  102. box->setTitle(tr::lng_ringtones_box_title());
  103. const auto container = box->verticalLayout();
  104. auto padding = st::boxPadding;
  105. padding.setTop(padding.bottom());
  106. struct State {
  107. AudioCreator creator;
  108. std::shared_ptr<Ui::RadiobuttonGroup> group;
  109. std::vector<std::shared_ptr<Data::DocumentMedia>> medias;
  110. Data::NotifySound chosen;
  111. base::unique_qptr<Ui::PopupMenu> menu;
  112. QPointer<Ui::Radiobutton> defaultButton;
  113. QPointer<Ui::Radiobutton> chosenButton;
  114. std::vector<QPointer<Ui::Radiobutton>> buttons;
  115. };
  116. const auto state = container->lifetime().make_state<State>(State{
  117. .group = std::make_shared<Ui::RadiobuttonGroup>(),
  118. .chosen = selected,
  119. });
  120. const auto addToGroup = [=](
  121. not_null<Ui::VerticalLayout*> verticalLayout,
  122. int value,
  123. const QString &text,
  124. bool chosen) {
  125. if (chosen) {
  126. state->group->setValue(value);
  127. }
  128. const auto button = verticalLayout->add(
  129. object_ptr<Ui::Radiobutton>(
  130. verticalLayout,
  131. state->group,
  132. value,
  133. text,
  134. st::defaultCheckbox),
  135. padding);
  136. if (chosen) {
  137. state->chosenButton = button;
  138. }
  139. if (value == kDefaultValue) {
  140. state->defaultButton = button;
  141. button->setClickedCallback([=] {
  142. Core::App().notifications().playSound(session, 0);
  143. });
  144. }
  145. if (value < 0) {
  146. return;
  147. }
  148. while (state->buttons.size() <= value) {
  149. state->buttons.push_back(nullptr);
  150. }
  151. button->setClickedCallback([=] {
  152. const auto media = state->medias[value].get();
  153. if (media->loaded()) {
  154. Core::App().notifications().playSound(
  155. session,
  156. media->owner()->id);
  157. }
  158. });
  159. base::install_event_filter(button, [=](not_null<QEvent*> e) {
  160. if (e->type() != QEvent::ContextMenu || state->menu) {
  161. return base::EventFilterResult::Continue;
  162. }
  163. state->menu = base::make_unique_q<Ui::PopupMenu>(
  164. button,
  165. st::popupMenuWithIcons);
  166. auto callback = [=] {
  167. const auto id = state->medias[value]->owner()->id;
  168. session->api().ringtones().remove(id);
  169. };
  170. state->menu->addAction(
  171. tr::lng_box_delete(tr::now),
  172. std::move(callback),
  173. &st::menuIconDelete);
  174. state->menu->popup(QCursor::pos());
  175. return base::EventFilterResult::Cancel;
  176. });
  177. };
  178. session->api().ringtones().uploadFails(
  179. ) | rpl::start_with_next([=](const QString &error) {
  180. if ((error == u"RINGTONE_DURATION_TOO_LONG"_q)) {
  181. box->getDelegate()->show(Ui::MakeInformBox(
  182. tr::lng_ringtones_error_max_duration(
  183. tr::now,
  184. lt_duration,
  185. Ui::FormatMuteFor(
  186. session->api().ringtones().maxDuration()))));
  187. } else if ((error == u"RINGTONE_SIZE_TOO_BIG"_q)) {
  188. box->getDelegate()->show(Ui::MakeInformBox(
  189. tr::lng_ringtones_error_max_size(
  190. tr::now,
  191. lt_size,
  192. Ui::FormatSizeText(
  193. session->api().ringtones().maxSize()))));
  194. } else if (error == u"RINGTONE_MIME_INVALID"_q) {
  195. box->getDelegate()->show(
  196. Ui::MakeInformBox(tr::lng_edit_media_invalid_file()));
  197. }
  198. }, box->lifetime());
  199. Ui::AddSubsectionTitle(
  200. container,
  201. tr::lng_ringtones_box_cloud_subtitle());
  202. const auto noSound = selected.none;
  203. addToGroup(
  204. container,
  205. kDefaultValue,
  206. tr::lng_ringtones_box_default(tr::now),
  207. false);
  208. addToGroup(
  209. container,
  210. kNoSoundValue,
  211. tr::lng_ringtones_box_no_sound(tr::now),
  212. noSound);
  213. const auto custom = container->add(
  214. object_ptr<Ui::VerticalLayout>(container));
  215. const auto rebuild = [=] {
  216. const auto old = base::take(state->medias);
  217. auto value = 0;
  218. while (custom->count()) {
  219. delete custom->widgetAt(0);
  220. }
  221. for (const auto &id : session->api().ringtones().list()) {
  222. const auto chosen = (state->chosen.id && state->chosen.id == id);
  223. const auto document = session->data().document(id);
  224. const auto text = ExtractRingtoneName(document);
  225. addToGroup(custom, value++, text, chosen);
  226. state->medias.push_back(document->createMediaView());
  227. document->owner().notifySettings().cacheSound(document);
  228. }
  229. custom->resizeToWidth(container->width());
  230. if (!state->chosenButton) {
  231. state->group->setValue(kDefaultValue);
  232. state->defaultButton->finishAnimating();
  233. }
  234. };
  235. session->api().ringtones().listUpdates(
  236. ) | rpl::start_with_next(rebuild, container->lifetime());
  237. session->api().ringtones().uploadDones(
  238. ) | rpl::start_with_next([=](DocumentId id) {
  239. state->chosen = Data::NotifySound{ .id = id };
  240. rebuild();
  241. }, container->lifetime());
  242. session->api().ringtones().requestList();
  243. rebuild();
  244. const auto upload = box->addRow(
  245. Settings::CreateButtonWithIcon(
  246. container,
  247. tr::lng_ringtones_box_upload_button(),
  248. st::ringtonesBoxButton,
  249. {
  250. &st::settingsIconAdd,
  251. Settings::IconType::Round,
  252. &st::windowBgActive
  253. }),
  254. style::margins());
  255. upload->addClickHandler([=] {
  256. const auto delay = st::ringtonesBoxButton.ripple.hideDuration;
  257. base::call_delayed(delay, crl::guard(box, [=] {
  258. const auto callback = [=](const FileDialog::OpenResult &result) {
  259. auto mime = QString();
  260. auto name = QString();
  261. auto content = result.remoteContent;
  262. if (!result.paths.isEmpty()) {
  263. auto info = QFileInfo(result.paths.front());
  264. mime = Core::MimeTypeForFile(info).name();
  265. name = info.fileName();
  266. auto f = QFile(result.paths.front());
  267. if (f.open(QIODevice::ReadOnly)) {
  268. content = f.readAll();
  269. f.close();
  270. }
  271. } else {
  272. mime = Core::MimeTypeForData(content).name();
  273. name = "audio";
  274. }
  275. const auto &ringtones = session->api().ringtones();
  276. if (int(content.size()) > ringtones.maxSize()) {
  277. box->getDelegate()->show(Ui::MakeInformBox(
  278. tr::lng_ringtones_error_max_size(
  279. tr::now,
  280. lt_size,
  281. Ui::FormatSizeText(ringtones.maxSize()))));
  282. return;
  283. }
  284. session->api().ringtones().upload(name, mime, content);
  285. };
  286. FileDialog::GetOpenPath(
  287. box.get(),
  288. tr::lng_ringtones_box_upload_choose(tr::now),
  289. "Audio files (*.mp3)",
  290. crl::guard(box, callback));
  291. }));
  292. });
  293. box->addSkip(st::ringtonesBoxSkip);
  294. Ui::AddDividerText(container, tr::lng_ringtones_box_about());
  295. box->addSkip(st::ringtonesBoxSkip);
  296. box->setWidth(st::boxWideWidth);
  297. box->addButton(tr::lng_settings_save(), [=] {
  298. const auto value = state->group->current();
  299. auto sound = (value == kDefaultValue)
  300. ? Data::NotifySound()
  301. : (value == kNoSoundValue)
  302. ? Data::NotifySound{ .none = true }
  303. : Data::NotifySound{ .id = state->medias[value]->owner()->id };
  304. save(sound);
  305. box->closeBox();
  306. });
  307. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  308. }
  309. void ThreadRingtonesBox(
  310. not_null<Ui::GenericBox*> box,
  311. not_null<Data::Thread*> thread) {
  312. const auto now = thread->owner().notifySettings().sound(thread);
  313. RingtonesBox(box, &thread->session(), now, [=](Data::NotifySound sound) {
  314. thread->owner().notifySettings().update(thread, {}, {}, sound);
  315. });
  316. }