1 // Copyright (c) 2020 Michael Fabian Dirks <info@xaymar.com>
2 //
3 // Permission is hereby granted, free of charge, to any person obtaining a copy
4 // of this software and associated documentation files (the "Software"), to deal
5 // in the Software without restriction, including without limitation the rights
6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 // copies of the Software, and to permit persons to whom the Software is
8 // furnished to do so, subject to the following conditions:
9 //
10 // The above copyright notice and this permission notice shall be included in all
11 // copies or substantial portions of the Software.
12 //
13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 // SOFTWARE.
20 
21 #include "ui-updater.hpp"
22 #include "common.hpp"
23 
24 #define ST_PREFIX "<ui::updater> "
25 #define D_LOG_ERROR(...) DLOG_ERROR(ST_PREFIX __VA_ARGS__)
26 #define D_LOG_WARNING(...) DLOG_WARNING(ST_PREFIX __VA_ARGS__)
27 #define D_LOG_INFO(...) DLOG_INFO(ST_PREFIX __VA_ARGS__)
28 #ifdef _DEBUG
29 #define D_LOG_DEBUG(...) DLOG_DEBUG(ST_PREFIX __VA_ARGS__)
30 #else
31 #define D_LOG_DEBUG(...)
32 #endif
33 
34 #define D_I18N_MENU_CHECKFORUPDATES "UI.Updater.Menu.CheckForUpdates"
35 #define D_I18N_MENU_CHECKFORUPDATES_AUTOMATICALLY "UI.Updater.Menu.CheckForUpdates.Automatically"
36 #define D_I18N_MENU_CHANNEL "UI.Updater.Menu.Channel"
37 #define D_I18N_MENU_CHANNEL_RELEASE "UI.Updater.Menu.Channel.Release"
38 #define D_I18N_MENU_CHANNEL_TESTING "UI.Updater.Menu.Channel.Testing"
39 #define D_I18N_DIALOG_TITLE "UI.Updater.Dialog.Title"
40 #define D_I18N_GITHUBPERMISSION_TITLE "UI.Updater.GitHubPermission.Title"
41 #define D_I18N_GITHUBPERMISSION_TEXT "UI.Updater.GitHubPermission.Text"
42 
updater_dialog()43 streamfx::ui::updater_dialog::updater_dialog() : QDialog(reinterpret_cast<QWidget*>(obs_frontend_get_main_window()))
44 {
45 	setupUi(this);
46 	setWindowFlag(Qt::WindowContextHelpButtonHint, false);
47 	setWindowFlag(Qt::WindowMinimizeButtonHint, false);
48 	setWindowFlag(Qt::WindowMaximizeButtonHint, false);
49 
50 	connect(ok, &QPushButton::clicked, this, &streamfx::ui::updater_dialog::on_ok);
51 	connect(cancel, &QPushButton::clicked, this, &streamfx::ui::updater_dialog::on_cancel);
52 }
53 
~updater_dialog()54 streamfx::ui::updater_dialog::~updater_dialog() {}
55 
show(streamfx::update_info current,streamfx::update_info update)56 void streamfx::ui::updater_dialog::show(streamfx::update_info current, streamfx::update_info update)
57 {
58 	{
59 		std::vector<char> buf;
60 		if (current.version_type) {
61 			buf.resize(static_cast<size_t>(snprintf(nullptr, 0, "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16,
62 													current.version_major, current.version_minor, current.version_patch,
63 													&current.version_type, current.version_index))
64 					   + 1);
65 			snprintf(buf.data(), buf.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16, current.version_major,
66 					 current.version_minor, current.version_patch, &current.version_type, current.version_index);
67 		} else {
68 			buf.resize(
69 				static_cast<size_t>(snprintf(nullptr, 0, "%" PRIu16 ".%" PRIu16 ".%" PRIu16, current.version_major,
70 											 current.version_minor, current.version_patch))
71 				+ 1);
72 			snprintf(buf.data(), buf.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16, current.version_major,
73 					 current.version_minor, current.version_patch);
74 		}
75 		currentVersion->setText(QString::fromUtf8(buf.data()));
76 	}
77 
78 	{
79 		std::vector<char> buf;
80 		if (update.version_type) {
81 			buf.resize(static_cast<size_t>(snprintf(nullptr, 0, "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16,
82 													update.version_major, update.version_minor, update.version_patch,
83 													&update.version_type, update.version_index))
84 					   + 1);
85 			snprintf(buf.data(), buf.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16, update.version_major,
86 					 update.version_minor, update.version_patch, &update.version_type, update.version_index);
87 		} else {
88 			buf.resize(static_cast<size_t>(snprintf(nullptr, 0, "%" PRIu16 ".%" PRIu16 ".%" PRIu16,
89 													update.version_major, update.version_minor, update.version_patch))
90 					   + 1);
91 			snprintf(buf.data(), buf.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16, update.version_major,
92 					 update.version_minor, update.version_patch);
93 		}
94 		latestVersion->setText(QString::fromUtf8(buf.data()));
95 
96 		{
97 			std::vector<char> buf2;
98 			buf2.resize(static_cast<size_t>(snprintf(nullptr, 0, D_TRANSLATE(D_I18N_DIALOG_TITLE), buf.data())) + 1);
99 			snprintf(buf2.data(), buf2.size(), D_TRANSLATE(D_I18N_DIALOG_TITLE), buf.data());
100 			setWindowTitle(QString::fromUtf8(buf2.data()));
101 		}
102 	}
103 
104 	_update_url = QUrl(QString::fromStdString(update.url));
105 
106 	this->setModal(true);
107 	QDialog::show();
108 }
109 
hide()110 void streamfx::ui::updater_dialog::hide()
111 {
112 	QDialog::hide();
113 	this->setModal(false);
114 }
115 
on_ok()116 void streamfx::ui::updater_dialog::on_ok()
117 {
118 	QDesktopServices::openUrl(_update_url);
119 	hide();
120 }
121 
on_cancel()122 void streamfx::ui::updater_dialog::on_cancel()
123 {
124 	hide();
125 }
126 
updater(QMenu * menu)127 streamfx::ui::updater::updater(QMenu* menu)
128 	: _updater(), _dialog(nullptr), _gdpr(nullptr), _cfu(nullptr), _cfu_auto(nullptr), _channel(nullptr),
129 	  _channel_menu(nullptr), _channel_stable(nullptr), _channel_preview(nullptr), _channel_group(nullptr)
130 {
131 	// Create dialog.
132 	_dialog = new updater_dialog();
133 
134 	{ // Create the necessary menu entries.
135 		menu->addSeparator();
136 
137 		// Check for Updates
138 		_cfu = menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHECKFORUPDATES)));
139 		connect(_cfu, &QAction::triggered, this, &streamfx::ui::updater::on_cfu_triggered);
140 
141 		// Automatically check for Updates
142 		_cfu_auto = menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHECKFORUPDATES_AUTOMATICALLY)));
143 		_cfu_auto->setCheckable(true);
144 		connect(_cfu_auto, &QAction::toggled, this, &streamfx::ui::updater::on_cfu_auto_toggled);
145 
146 		// Update Channel
147 		_channel_menu = menu->addMenu(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL)));
148 
149 		_channel_stable = _channel_menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL_RELEASE)));
150 		_channel_stable->setCheckable(true);
151 
152 		_channel_preview = _channel_menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL_TESTING)));
153 		_channel_preview->setCheckable(true);
154 
155 		_channel_group = new QActionGroup(_channel_menu);
156 		_channel_group->addAction(_channel_stable);
157 		_channel_group->addAction(_channel_preview);
158 		connect(_channel_group, &QActionGroup::triggered, this, &streamfx::ui::updater::on_channel_group_triggered);
159 	}
160 
161 	// Connect internal signals.
162 	connect(this, &streamfx::ui::updater::autoupdate_changed, this, &streamfx::ui::updater::on_autoupdate_changed,
163 			Qt::QueuedConnection);
164 	connect(this, &streamfx::ui::updater::channel_changed, this, &streamfx::ui::updater::on_channel_changed,
165 			Qt::QueuedConnection);
166 	connect(this, &streamfx::ui::updater::update_detected, this, &streamfx::ui::updater::on_update_detected,
167 			Qt::QueuedConnection);
168 	connect(this, &streamfx::ui::updater::check_active, this, &streamfx::ui::updater::on_check_active,
169 			Qt::QueuedConnection);
170 
171 	{ // Retrieve the updater object and listen to it.
172 		_updater = streamfx::updater::instance();
173 		_updater->events.automation_changed.add(std::bind(&streamfx::ui::updater::on_updater_automation_changed, this,
174 														  std::placeholders::_1, std::placeholders::_2));
175 		_updater->events.channel_changed.add(std::bind(&streamfx::ui::updater::on_updater_channel_changed, this,
176 													   std::placeholders::_1, std::placeholders::_2));
177 		_updater->events.refreshed.add(
178 			std::bind(&streamfx::ui::updater::on_updater_refreshed, this, std::placeholders::_1));
179 
180 		// Sync with updater information.
181 		emit autoupdate_changed(_updater->automation());
182 		emit channel_changed(_updater->channel());
183 	}
184 }
185 
~updater()186 streamfx::ui::updater::~updater() {}
187 
on_updater_automation_changed(streamfx::updater &,bool value)188 void streamfx::ui::updater::on_updater_automation_changed(streamfx::updater&, bool value)
189 {
190 	emit autoupdate_changed(value);
191 }
192 
on_updater_channel_changed(streamfx::updater &,streamfx::update_channel channel)193 void streamfx::ui::updater::on_updater_channel_changed(streamfx::updater&, streamfx::update_channel channel)
194 {
195 	emit channel_changed(channel);
196 }
197 
on_updater_refreshed(streamfx::updater &)198 void streamfx::ui::updater::on_updater_refreshed(streamfx::updater&)
199 {
200 	emit check_active(false);
201 
202 	if (!_updater->have_update())
203 		return;
204 
205 	emit update_detected();
206 }
207 
obs_ready()208 void streamfx::ui::updater::obs_ready()
209 {
210 	if (_updater->automation()) {
211 		if (_updater->gdpr()) {
212 			_updater->refresh();
213 		} else {
214 			create_gdpr_box();
215 			_gdpr->exec();
216 		}
217 	}
218 }
219 
on_channel_changed(streamfx::update_channel channel)220 void streamfx::ui::updater::on_channel_changed(streamfx::update_channel channel)
221 {
222 	bool is_stable = channel == streamfx::update_channel::RELEASE;
223 	_channel_stable->setChecked(is_stable);
224 	_channel_preview->setChecked(!is_stable);
225 }
226 
on_update_detected()227 void streamfx::ui::updater::on_update_detected()
228 {
229 	_dialog->show(_updater->get_current_info(), _updater->get_update_info());
230 }
231 
on_autoupdate_changed(bool enabled)232 void streamfx::ui::updater::on_autoupdate_changed(bool enabled)
233 {
234 	_cfu_auto->setChecked(enabled);
235 }
236 
on_gdpr_button(QAbstractButton * btn)237 void streamfx::ui::updater::on_gdpr_button(QAbstractButton* btn)
238 {
239 	if (_gdpr->standardButton(btn) == QMessageBox::Ok) {
240 		_updater->set_gdpr(true);
241 		emit check_active(true);
242 		_updater->refresh();
243 	} else {
244 		_updater->set_gdpr(false);
245 		_updater->set_automation(false);
246 	}
247 }
248 
on_cfu_triggered(bool)249 void streamfx::ui::updater::on_cfu_triggered(bool)
250 {
251 	if (!_updater->gdpr()) {
252 		create_gdpr_box();
253 		_gdpr->exec();
254 	} else {
255 		emit check_active(true);
256 		_updater->refresh();
257 	}
258 }
259 
on_cfu_auto_toggled(bool flag)260 void streamfx::ui::updater::on_cfu_auto_toggled(bool flag)
261 {
262 	_updater->set_automation(flag);
263 }
264 
on_channel_group_triggered(QAction * action)265 void streamfx::ui::updater::on_channel_group_triggered(QAction* action)
266 {
267 	if (action == _channel_stable) {
268 		_updater->set_channel(update_channel::RELEASE);
269 	} else {
270 		_updater->set_channel(update_channel::TESTING);
271 	}
272 }
273 
instance(QMenu * menu)274 std::shared_ptr<streamfx::ui::updater> streamfx::ui::updater::instance(QMenu* menu)
275 {
276 	static std::weak_ptr<streamfx::ui::updater> _instance;
277 	static std::mutex                           _lock;
278 
279 	auto lock = std::lock_guard<std::mutex>(_lock);
280 	if (_instance.expired() && menu) {
281 		auto ptr  = std::make_shared<streamfx::ui::updater>(menu);
282 		_instance = ptr;
283 		return ptr;
284 	} else {
285 		return _instance.lock();
286 	}
287 }
288 
on_check_active(bool active)289 void streamfx::ui::updater::on_check_active(bool active)
290 {
291 	_cfu->setEnabled(!active);
292 	_channel_group->setEnabled(!active);
293 	_channel_preview->setEnabled(!active);
294 	_channel_stable->setEnabled(!active);
295 	_channel_menu->setEnabled(!active);
296 }
297 
create_gdpr_box()298 void streamfx::ui::updater::create_gdpr_box()
299 {
300 	if (_gdpr) {
301 		_gdpr->deleteLater();
302 		_gdpr = nullptr;
303 	}
304 
305 	// Create GitHub message box.
306 	_gdpr = new QMessageBox(reinterpret_cast<QWidget*>(obs_frontend_get_main_window()));
307 	_gdpr->setWindowTitle(QString::fromUtf8(D_TRANSLATE(D_I18N_GITHUBPERMISSION_TITLE)));
308 	_gdpr->setTextFormat(Qt::TextFormat::RichText);
309 	_gdpr->setText(QString::fromUtf8(D_TRANSLATE(D_I18N_GITHUBPERMISSION_TEXT)));
310 	_gdpr->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
311 	connect(_gdpr, &QMessageBox::buttonClicked, this, &streamfx::ui::updater::on_gdpr_button);
312 }
313