]> git.sur5r.net Git - minitube/commitdiff
New upstream version 3.5
authorJakob Haufe <jakob@haufe.it>
Tue, 29 Sep 2020 08:49:42 +0000 (10:49 +0200)
committerJakob Haufe <jakob@haufe.it>
Tue, 29 Sep 2020 08:49:42 +0000 (10:49 +0200)
135 files changed:
lib/http/.clang-format [new file with mode: 0644]
lib/http/.gitignore
lib/http/CMakeLists.txt [new file with mode: 0644]
lib/http/LICENSE [new file with mode: 0644]
lib/http/README.md
lib/http/http.pri
lib/http/marketplace.json [new file with mode: 0644]
lib/http/src/cachedhttp.cpp
lib/http/src/cachedhttp.h
lib/http/src/http.cpp
lib/http/src/http.h
lib/media/COPYING [new file with mode: 0644]
lib/media/README.md
lib/media/media.pri
lib/media/src/mpv/mediampv.cpp
lib/updater/.clang-format [new file with mode: 0644]
lib/updater/.gitignore [new file with mode: 0644]
lib/updater/COPYING [new file with mode: 0644]
lib/updater/README.md [new file with mode: 0644]
lib/updater/src/impl/appcastparser.cpp [new file with mode: 0644]
lib/updater/src/impl/appcastparser.h [new file with mode: 0644]
lib/updater/src/impl/checker.cpp [new file with mode: 0644]
lib/updater/src/impl/checker.h [new file with mode: 0644]
lib/updater/src/impl/defaultupdater.cpp [new file with mode: 0644]
lib/updater/src/impl/defaultupdater.h [new file with mode: 0644]
lib/updater/src/impl/dialog.cpp [new file with mode: 0644]
lib/updater/src/impl/dialog.h [new file with mode: 0644]
lib/updater/src/impl/downloader.cpp [new file with mode: 0644]
lib/updater/src/impl/downloader.h [new file with mode: 0644]
lib/updater/src/impl/installer.h [new file with mode: 0644]
lib/updater/src/impl/parser.h [new file with mode: 0644]
lib/updater/src/impl/runinstaller.cpp [new file with mode: 0644]
lib/updater/src/impl/runinstaller.h [new file with mode: 0644]
lib/updater/src/impl/simplexmlparser.cpp [new file with mode: 0644]
lib/updater/src/impl/simplexmlparser.h [new file with mode: 0644]
lib/updater/src/sparkle/sparkleupdater.h [new file with mode: 0644]
lib/updater/src/sparkle/sparkleupdater.mm [new file with mode: 0644]
lib/updater/src/updater.cpp [new file with mode: 0644]
lib/updater/src/updater.h [new file with mode: 0644]
lib/updater/updater.pri [new file with mode: 0644]
lib/updater/updater.pro [new file with mode: 0644]
locale/ar.ts
locale/ast.ts
locale/be.ts
locale/bg_BG.ts
locale/ca.ts
locale/ca_ES.ts
locale/cs_CZ.ts
locale/da.ts
locale/de_DE.ts
locale/el.ts
locale/en_GB.ts
locale/es.ts
locale/es_AR.ts
locale/es_ES.ts
locale/es_MX.ts
locale/fi.ts
locale/fi_FI.ts
locale/fr.ts
locale/gl.ts
locale/he_IL.ts
locale/hr.ts
locale/hu.ts
locale/id.ts
locale/it.ts
locale/ja_JP.ts
locale/ko_KR.ts
locale/ky.ts
locale/ms_MY.ts
locale/nb.ts
locale/nl.ts
locale/nn.ts
locale/pl.ts
locale/pl_PL.ts
locale/pt.ts
locale/pt_BR.ts
locale/pt_PT.ts
locale/ro.ts
locale/ru.ts
locale/sk.ts
locale/sl.ts
locale/sq.ts
locale/sr.ts
locale/sv_SE.ts
locale/th.ts
locale/tr.ts
locale/uk.ts
locale/uk_UA.ts
locale/vi.ts
locale/zh_CN.ts
locale/zh_TW.ts
minitube.pro
src/aboutview.cpp
src/appwidget.cpp
src/channelaggregator.cpp
src/channelview.cpp
src/httputils.cpp
src/invidious/invidious.cpp [new file with mode: 0644]
src/invidious/invidious.h [new file with mode: 0644]
src/invidious/invidious.pri [new file with mode: 0644]
src/invidious/ivchannel.cpp [new file with mode: 0644]
src/invidious/ivchannel.h [new file with mode: 0644]
src/invidious/ivchannelsource.cpp [new file with mode: 0644]
src/invidious/ivchannelsource.h [new file with mode: 0644]
src/invidious/ivlistparser.cpp [new file with mode: 0644]
src/invidious/ivlistparser.h [new file with mode: 0644]
src/invidious/ivsearch.cpp [new file with mode: 0644]
src/invidious/ivsearch.h [new file with mode: 0644]
src/invidious/ivsinglevideosource.cpp [new file with mode: 0644]
src/invidious/ivsinglevideosource.h [new file with mode: 0644]
src/invidious/ivvideolist.cpp [new file with mode: 0644]
src/invidious/ivvideolist.h [new file with mode: 0644]
src/main.cpp
src/mainwindow.cpp
src/mainwindow.h
src/mediaview.cpp
src/messagebar.cpp
src/messagebar.h
src/playlistmodel.cpp
src/playlistmodel.h
src/searchview.cpp
src/standardfeedsview.cpp
src/updatechecker.cpp [deleted file]
src/updatechecker.h [deleted file]
src/updateutils.cpp [new file with mode: 0644]
src/updateutils.h [new file with mode: 0644]
src/videoapi.h [new file with mode: 0644]
src/videosource.h
src/videosourcewidget.cpp
src/waitingspinnerwidget.cpp [new file with mode: 0644]
src/waitingspinnerwidget.h [new file with mode: 0644]
src/yt3.cpp
src/ytchannel.cpp
src/ytsearch.h
src/ytvideo.cpp

diff --git a/lib/http/.clang-format b/lib/http/.clang-format
new file mode 100644 (file)
index 0000000..2409f52
--- /dev/null
@@ -0,0 +1,11 @@
+BasedOnStyle: LLVM
+IndentWidth: 4
+AccessModifierOffset: -4
+ColumnLimit: 100
+AllowShortIfStatementsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: Inline
+KeepEmptyLinesAtTheStartOfBlocks: false
+ContinuationIndentWidth: 8
+AlignAfterOpenBracket: true
+BinPackParameters: false
+AllowAllParametersOfDeclarationOnNextLine: false
index e31cfb294b9ee2f7cb5f2e42bcdffa353fb535bc..f231897212e2319271d463770680143690c31e35 100644 (file)
@@ -1,2 +1,5 @@
-
+.idea
+cmake-build-debug
 *.user
+.DS_Store
+build
diff --git a/lib/http/CMakeLists.txt b/lib/http/CMakeLists.txt
new file mode 100644 (file)
index 0000000..7804a22
--- /dev/null
@@ -0,0 +1,25 @@
+cmake_minimum_required(VERSION 3.12)
+project(http LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_AUTOMOC ON)
+
+find_package(Qt5 REQUIRED COMPONENTS Core Network)
+
+add_library(http
+       src/cachedhttp.h
+       src/cachedhttp.cpp
+       src/http.h
+       src/http.cpp
+       src/httpreply.h
+       src/httprequest.h
+       src/httpreply.cpp
+       src/localcache.h
+       src/localcache.cpp
+       src/networkhttpreply.h
+       src/networkhttpreply.cpp
+       src/throttledhttp.h
+       src/throttledhttp.cpp
+)
+
+target_link_libraries(http Qt5::Network)
diff --git a/lib/http/LICENSE b/lib/http/LICENSE
new file mode 100644 (file)
index 0000000..e3fd3da
--- /dev/null
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Flavio Tordini
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
index a1370ff0e038bdf065269777b9aee5405c0e748d..efb936d6bf0f99d344e3c278d3fde4de79bca6ee 100644 (file)
 # A wrapper for the Qt Network Access API
 
-This is just a wrapper around Qt's QNetworkAccessManager and friends. I use it in my Qt apps at http://flavio.tordini.org . It allows me to add missing functionality as needed, e.g.:
+This is just a wrapper around Qt's QNetworkAccessManager and friends. I use it in my apps at https://flavio.tordini.org . It has a simpler, higher-level API and some functionality not found in Qt:
 
 - Throttling (as required by many web APIs nowadays)
-- Read timeouts (don't let your requests get stuck forever)
 - Automatic retries
 - User agent and request header defaults
 - Partial requests
+- Easier POST requests
+- Read timeouts (don't let your requests get stuck forever). (now supported by Qt >= 5.15)
 - Redirection support (now supported by Qt >= 5.6)
 
-It has a simpler, higher-level API that I find easier to work with. The design emerged naturally in years of practical use.
+## Design
 
-A basic example:
+This library uses the [Decorator design pattern](https://en.wikipedia.org/wiki/Decorator_pattern) to modularize features and make it easy to add them and use them as needed. The main class is [Http](https://github.com/flaviotordini/http/blob/master/src/http.h), which implements the base features of a HTTP client. More specialized classes are:
 
+- [CachedHttp](https://github.com/flaviotordini/http/blob/master/src/cachedhttp.h), a simple disk-based cache
+- [ThrottledHttp](https://github.com/flaviotordini/http/blob/master/src/throttledhttp.h), implements request throttling (aka limiting)
+
+The constructor of these classes takes another Http instance for which they will act as a proxy. (See examples below). Following this design you can create your own Http subclass. For example, a different caching mechanism, an event dispatcher, custom request logging, etc.
+
+
+## Build Instructions
+In order to build this library you can use either `qmake` or `cmake`.
+
+### qmake
+```
+mkdir build
+cd build
+qmake ..
+make
 ```
-QObject *reply = Http::instance().get("https://google.com/");
-connect(reply, SIGNAL(data(QByteArray)), SLOT(onSuccess(QByteArray)));
-connect(reply, SIGNAL(error(QString)), SLOT(onError(QString)));
 
-void MyClass::onSuccess(const QByteArray &bytes) {
-       qDebug() << "Feel the bytes!" << bytes;
-}
+### CMake
+```
+mkdir build
+cd build
+cmake ..
+make
+```
+
+## Integration
+
+You can use this library as a git submodule. For example, add it to your project inside a lib subdirectory:
 
-void MyClass::onError(const QString &message) {
-       qDebug() << "Something's wrong here" << message;
-}
+```
+git submodule add -b master https://github.com/flaviotordini/http lib/http
+```
+
+Then you can update your git submodules like this:
+
+```
+git submodule update --init --recursive --remote
 ```
 
-This is a real-world example of building a Http object suitable to a web service. It throttles requests, uses a custom user agent and caches results:
+To integrate the library in your qmake based project just add this to your .pro file:
 
 ```
+include(lib/http/http.pri)
+```
+
+qmake builds all object files in the same directory. In order to avoid filename clashes use:
+
+```
+CONFIG += object_parallel_to_source
+```
+
+If you are using CMake you can integrate the library by adding the following lines to your CMakeLists.txt:
+
+```
+add_subdirectory(lib/http)
+...
+target_link_library(your_super_cool_project <other libraries> http)
+```
+or if you have installed http you can find it via:
+
+```
+find_library(http REQUIRED)
+...
+target_link_library(your_super_cool_project <other libraries> http)
+```
+
+
+## Examples
+
+A basic C++14 example:
+
+```
+#include "http.h"
+
+auto reply = Http::instance().get("https://google.com/");
+connect(reply, &HttpReply::finished, this, [](auto &reply) {
+    if (reply.isSuccessful()) {
+        qDebug() << "Feel the bytes!" << reply.body();
+    } else {
+        qDebug() << "Something's wrong here" << reply.statusCode() << reply.reasonPhrase();
+    }
+});
+```
+
+This is a real-world example of building a Http object with more complex features. It throttles requests, uses a custom user agent and caches results:
+
+```
+#include "http.h"
+#include "cachedhttp.h"
+#include "throttledhttp.h"
+
 Http &myHttp() {
     static Http *http = [] {
         Http *http = new Http;
@@ -50,14 +125,16 @@ Http &myHttp() {
 If the full power (and complexity) of QNetworkReply is needed you can always fallback to it:
 
 ```
+#include "http.h"
+
 HttpRequest req;
 req.url = "https://flavio.tordini.org/";
 QNetworkReply *reply = Http::instance().networkReply(req);
 // Use QNetworkReply as needed...
 ```
 
-You can use this library under the MIT license and at your own risk. If you do, you're welcome contributing your changes and fixes.
+Note that features like redirection, retries and read timeouts won't work in this mode.
 
-Cheers,
+## License
 
-Flavio
+You can use this library under the MIT license and at your own risk. If you do, you're welcome contributing your changes and fixes.
index b17f3ba4d3c0edcb0be8e9195cc793bc4686e6ba..fa28486d7f75fca303cd212955d08b118fb5d49e 100644 (file)
@@ -2,6 +2,7 @@ QT *= network
 
 INCLUDEPATH += $$PWD/src
 DEPENDPATH += $$PWD/src
+DEFINES += HTTP
 
 HEADERS += \
     $$PWD/src/cachedhttp.h \
diff --git a/lib/http/marketplace.json b/lib/http/marketplace.json
new file mode 100644 (file)
index 0000000..15d9d5d
--- /dev/null
@@ -0,0 +1,50 @@
+{
+    "$schema": "http://qt.io/schema/extension-schema-v1#",
+    "title": "Http",
+    "extensionType": [
+        "library"
+    ],
+    "version": "1",
+    "vendor": {
+        "name": "Flavio Tordini",
+        "url": "https://flavio.tordini.org"
+    },
+    "contact": "Flavio Tordini <flavio.tordini@gmail.com>",
+    "copyright": [
+        "Flavio Tordini"
+    ],
+    "author": "Flavio Tordini",
+    "icon": "https://flavio.tordini.org/favicon-196x196.png",
+    "licenses": [
+        {
+            "licenseType": "MIT",
+            "licenseUrl": "https://opensource.org/licenses/MIT"
+        }
+    ],
+    "created": "2016-07-02",
+    "platforms": [
+        "Windows",
+        "Linux",
+        "macOS",
+        "Android",
+        "iOS"
+    ],
+    "qtVersions": [
+        "5.10.0-or-later"
+    ],
+    "tags": [
+        "http,web,rest,networking,freeproduct,tools,utility"
+    ],
+    "price": {
+        "listprice": 0
+    },
+    "support": "flavio.tordini@gmail.com",
+    "bugUrl": "https://github.com/flaviotordini/http/issues",
+    "sourceRepoUrl": "https://github.com/flaviotordini/http",
+    "userManuals": [
+        "https://github.com/flaviotordini/http/blob/master/README.md"
+    ],
+    "dependencies": [
+        "Network"
+    ]
+}
\ No newline at end of file
index 9762143e56c59c73db4145ba2e4adc2a91b2b5a6..e05534209c47e95ef7d1deff9388b7daad9579f9 100644 (file)
@@ -1,19 +1,6 @@
 #include "cachedhttp.h"
 #include "localcache.h"
 
-namespace {
-
-QByteArray requestHash(const HttpRequest &req) {
-    const char sep = '|';
-    QByteArray s = req.url.toEncoded() + sep + req.body + sep + QByteArray::number(req.offset);
-    if (req.operation == QNetworkAccessManager::PostOperation) {
-        s.append(sep);
-        s.append("POST");
-    }
-    return LocalCache::hash(s);
-}
-} // namespace
-
 CachedHttpReply::CachedHttpReply(const QByteArray &body, const HttpRequest &req)
     : bytes(body), req(req) {
     QTimer::singleShot(0, this, SLOT(emitSignals()));
@@ -69,3 +56,20 @@ HttpReply *CachedHttp::request(const HttpRequest &req) {
     qDebug() << "MISS" << key << req.url;
     return new WrappedHttpReply(cache, key, http.request(req));
 }
+
+QByteArray CachedHttp::requestHash(const HttpRequest &req) {
+    const char sep = '|';
+
+    QByteArray s;
+    if (ignoreHostname) {
+        s = (req.url.scheme() + sep + req.url.path() + sep + req.url.query()).toUtf8();
+    } else {
+        s = req.url.toEncoded();
+    }
+    s += sep + req.body + sep + QByteArray::number(req.offset);
+    if (req.operation == QNetworkAccessManager::PostOperation) {
+        s.append(sep);
+        s.append("POST");
+    }
+    return LocalCache::hash(s);
+}
index e487a17d38dc2f51789b37771990bd6128d8bea6..01fef133a624386eccf50072f6bb94d85c788abb 100644 (file)
@@ -11,12 +11,16 @@ public:
     void setMaxSeconds(uint seconds);
     void setMaxSize(uint maxSize);
     void setCachePostRequests(bool value) { cachePostRequests = value; }
+    void setIgnoreHostname(bool value) { ignoreHostname = value; }
     HttpReply *request(const HttpRequest &req);
 
 private:
+    QByteArray requestHash(const HttpRequest &req);
+
     Http &http;
     LocalCache *cache;
     bool cachePostRequests;
+    bool ignoreHostname = false;
 };
 
 class CachedHttpReply : public HttpReply {
index f724889352e67e2e0cfdacd7f31b1a40e85facc9..5df34a624a88b36b0590905c186a426e92247dfc 100644 (file)
@@ -81,6 +81,14 @@ QNetworkReply *Http::networkReply(const HttpRequest &req) {
         networkReply = manager->post(request, req.body);
         break;
 
+    case QNetworkAccessManager::PutOperation:
+        networkReply = manager->put(request, req.body);
+        break;
+
+    case QNetworkAccessManager::DeleteOperation:
+        networkReply = manager->deleteResource(request);
+        break;
+
     default:
         qWarning() << "Unknown operation:" << req.operation;
     }
@@ -140,12 +148,32 @@ HttpReply *Http::post(const QUrl &url, const QByteArray &body, const QByteArray
     return request(req);
 }
 
-int Http::getMaxRetries() const
-{
+
+HttpReply *Http::put(const QUrl &url, const QByteArray &body, const QByteArray &contentType) {
+       HttpRequest req;
+       req.url = url;
+       req.operation = QNetworkAccessManager::PutOperation;
+       req.body = body;
+       req.headers = requestHeaders;
+       QByteArray cType = contentType;
+       if (cType.isEmpty()) cType = "application/x-www-form-urlencoded";
+       req.headers.insert("Content-Type", cType);
+       return request(req);
+}
+
+
+HttpReply *Http::deleteResource(const QUrl &url) {
+       HttpRequest req;
+       req.url = url;
+       req.operation = QNetworkAccessManager::DeleteOperation;
+       req.headers = requestHeaders;
+       return request(req);
+}
+
+int Http::getMaxRetries() const {
     return maxRetries;
 }
 
-void Http::setMaxRetries(int value)
-{
+void Http::setMaxRetries(int value) {
     maxRetries = value;
 }
index 8ae56cf764b254c197d9bec6b69dedfc14a882fb..466887c9d06d1ac83037389d049998129cef1d25 100644 (file)
@@ -35,6 +35,8 @@ public:
     HttpReply *head(const QUrl &url);
     HttpReply *post(const QUrl &url, const QMap<QString, QString> &params);
     HttpReply *post(const QUrl &url, const QByteArray &body, const QByteArray &contentType);
+    HttpReply *put(const QUrl &url, const QByteArray &body, const QByteArray &contentType);
+    HttpReply *deleteResource(const QUrl &url);
 
 private:
     QMap<QByteArray, QByteArray> requestHeaders;
diff --git a/lib/media/COPYING b/lib/media/COPYING
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
index 4aeec3051e607abda23b629e550f84114ede9e45..31682992a44f031b5dbe84fa8653b9b49a0d1ff7 100644 (file)
@@ -1,9 +1,15 @@
 # Qt Media Library Abstraction
 
-This is a simple wrapper around a multimedia playback library.
+This is a simple wrapper around a multimedia playback library. I use it in my apps at https://flavio.tordini.org
+
+The most interesting and maintained backend is MPV. It works great on macOS, Windows and Linux.
+
+I wrote this high level wrapper because I was not happy with Qt Multimedia and its lack of a common backend and guaranteed media format support across desktop platforms.
 
 Define `MEDIA_QTAV` to link to QtAV or `MEDIA_MPV` to link to libmpv (>=0.29.0).
 
 `MEDIA_AUDIOONLY` can be defined if the application does not need video.
 
-You can use this library under the MIT license and at your own risk. If you do, you're welcome contributing your changes and fixes.
+You can use this library under the GPLv3 license. If you do, you're welcome contributing your changes and fixes.
+
+If you would like to use this library in a commercial project, contact me at flavio.tordini@gmail.com
\ No newline at end of file
index f6b64fa7e56adf57467399fcedba741cd0390003..a20f396a1d7b2a2ea17cec3b3a4410208e03d7f6 100644 (file)
@@ -7,8 +7,8 @@ contains(DEFINES, MEDIA_QTAV) {
     QT += avwidgets
     INCLUDEPATH += $$PWD/src/qtav
     DEPENDPATH += $$PWD/src/qtav
-    HEADERS += $$PWD/src/mediaqtav.h
-    SOURCES += $$PWD/src/mediaqtav.cpp
+    HEADERS += $$PWD/src/qtav/mediaqtav.h
+    SOURCES += $$PWD/src/qtav/mediaqtav.cpp
 }
 
 contains(DEFINES, MEDIA_MPV) {
index 3276f4b72aa153767641b849ac04ccb87f87c38d..62915110bdceb01c0671d11c159ab9ddb26a80af 100644 (file)
@@ -158,7 +158,7 @@ void MediaMPV::handleMpvEvent(mpv_event *event) {
 
     case MPV_EVENT_PROPERTY_CHANGE: {
         mpv_event_property *prop = (mpv_event_property *)event->data;
-        qDebug() << prop->name << prop->data;
+        // qDebug() << prop->name << prop->data;
 
         if (strcmp(prop->name, "time-pos") == 0) {
             if (prop->format == MPV_FORMAT_DOUBLE) {
diff --git a/lib/updater/.clang-format b/lib/updater/.clang-format
new file mode 100644 (file)
index 0000000..2409f52
--- /dev/null
@@ -0,0 +1,11 @@
+BasedOnStyle: LLVM
+IndentWidth: 4
+AccessModifierOffset: -4
+ColumnLimit: 100
+AllowShortIfStatementsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: Inline
+KeepEmptyLinesAtTheStartOfBlocks: false
+ContinuationIndentWidth: 8
+AlignAfterOpenBracket: true
+BinPackParameters: false
+AllowAllParametersOfDeclarationOnNextLine: false
diff --git a/lib/updater/.gitignore b/lib/updater/.gitignore
new file mode 100644 (file)
index 0000000..09e07f4
--- /dev/null
@@ -0,0 +1,3 @@
+
+.DS_Store
+.vscode
diff --git a/lib/updater/COPYING b/lib/updater/COPYING
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/lib/updater/README.md b/lib/updater/README.md
new file mode 100644 (file)
index 0000000..76f3b7d
--- /dev/null
@@ -0,0 +1,126 @@
+# An updater for Qt apps
+
+This is an extensible updater for Qt apps. It can wrap Sparkle on macOS and use its own implementation on Windows and Linux. I use it in my apps at https://flavio.tordini.org .
+
+## Design
+
+The main interface is [Updater](https://github.com/flaviotordini/updater/blob/master/src/updater.h). A shared Updater subclass instance should be set on startup using `Updater::setInstance()`. Available implementations are:
+
+- [`updater::DefaultUpdater`](https://github.com/flaviotordini/updater/blob/master/src/impl/defaultupdater.h), the default Qt-based implementation.
+
+- [`updater::SparkleUpdater`](https://github.com/flaviotordini/updater/blob/master/src/sparkle/sparkleupdater.h), a Sparkle-based implementation for macOS
+
+## User Interface
+
+### Built-in Widgets
+
+Updater provides ready-to-use widgets:
+
+- `Updater::getAction()` returns a QAction suitable to be inserted in a QMenu.
+- `Updater::getLabel()` returns a QLabel that automatically changes its message. Typically used in the about box.
+- `Updater::getButton()` returns a QPushButton that autohides or automatically changes its function depending on the Updater status.
+
+When the user triggers the action or pushes the button a dialog will show which is dependent on the Updater implementation.
+
+## Entension Points
+
+[updater::DefaultUpdater](https://github.com/flaviotordini/updater/blob/master/src/impl/defaultupdater.h) has a number of extension points so it can be adapted to different release manifest formats and update mechanisms.
+
+### Parser
+
+Implement [updater::Parser](https://github.com/flaviotordini/updater/blob/master/src/impl/parser.h) to parse your own manifest format. There are two ready-to-use parsers:
+
+- [updater::AppcastParser](https://github.com/flaviotordini/updater/blob/master/src/impl/appcastparser.h). This the appcast format also used by Sparkle. It's a RSS feed with Sparkle extensions.
+- [updater::SimpleXmlParser](https://github.com/flaviotordini/updater/blob/master/src/impl/simplexmlparser.h). This is a very simple XML format
+
+Set the desired Parser implementation using `updater::DefaultUpdater::setParser`. The default is [updater::AppcastParser].
+
+### Installer
+
+[updater::Installer](https://github.com/flaviotordini/updater/blob/master/src/impl/installer.h) is the abstraction responsible for preparing and running the update process. Currently the only available Installer implementation is [updater::RunInstaller](https://github.com/flaviotordini/updater/blob/master/src/impl/runinstaller.h). It just runs an executable update payload, optionally with arguments.
+
+Installer can be implemented in other ways, for example an Installer that unzips a payload and moves files. Or one that invokes an update helper. Another idea is signature validation.
+
+Set the desired Installer implementation using `updater::DefaultUpdater::setInstaller`. The default is [updater::RunInstaller].
+
+## Build Instructions
+
+### qmake
+```
+mkdir build
+cd build
+qmake ..
+make
+```
+
+## Integration
+
+You can use this library as a git submodule. For example, add it to your project inside a lib subdirectory:
+
+```
+git submodule add -b master https://github.com/flaviotordini/updater lib/updater
+```
+
+Then you can update your git submodules like this:
+
+```
+git submodule update --init --recursive --remote
+```
+
+To integrate the library in your qmake based project just add this to your .pro file:
+
+```
+include(lib/updater/updater.pri)
+```
+
+qmake builds all object files in the same directory. In order to avoid filename clashes use:
+
+```
+CONFIG += object_parallel_to_source
+```
+
+## Examples
+
+Example setup of the shared Updater instance:
+
+```
+#include "updater.h"
+#ifdef UPDATER_SPARKLE
+#include "sparkleupdater.h"
+#else
+#include "defaultupdater.h"
+#endif
+
+void setupUpdater() {
+    #ifdef UPDATER_SPARKLE
+        Updater::setInstance(new updater::SparkleUpdater());
+    #else
+        auto updater = new updater::DefaultUpdater();
+        updater->setManifestUrl(myAppcastUrl);
+        Updater::setInstance(updater);
+    #endif
+}
+```
+
+Updater provides a QAction instance ready to be used in a menu.
+
+```
+myMenu->addAction(Updater::instance().getAction());
+```
+
+In the About box you can use the standard widgets provided by Updater. A QLabel and a QPushButton.
+
+```
+myLayout->addWidget(Updater::instance().getLabel());
+myLayout->addWidget(Updater::instance().getButton());
+```
+
+## Security
+
+Always serve your manifest files and binary updates via HTTPS.
+
+## License
+
+You can use this library under the GPLv3 license terms. If you do, you're welcome contributing your changes and fixes. Donations are welcome at https://flavio.tordini.org/donate
+
+For commercial projects I ask for a one-time license fee, contact me at flavio.tordini@gmail.com
diff --git a/lib/updater/src/impl/appcastparser.cpp b/lib/updater/src/impl/appcastparser.cpp
new file mode 100644 (file)
index 0000000..2cf841e
--- /dev/null
@@ -0,0 +1,43 @@
+#include "appcastparser.h"
+
+#include "defaultupdater.h"
+
+namespace updater {
+
+AppcastParser::AppcastParser() {}
+
+void AppcastParser::parse(const QByteArray &bytes) {
+    error = false;
+    errorMessage.clear();
+
+    const QLatin1String sparkleNS("http://www.andymatuschak.org/xml-namespaces/sparkle");
+
+    QXmlStreamReader reader(bytes);
+    while (!reader.atEnd()) {
+        reader.readNext();
+        if (reader.name() == QLatin1String("item")) {
+            while (reader.readNextStartElement()) {
+                if (reader.name() == QLatin1String("enclosure")) {
+                    auto attrs = reader.attributes();
+                    QString url = attrs.value(QLatin1String("url")).toString();
+                    updater->setDownloadUrl(url);
+
+                    QString version = attrs.value(sparkleNS, QLatin1String("version")).toString();
+                    updater->setVersion(version);
+
+                    QString shortVersionString =
+                            attrs.value(sparkleNS, QLatin1String("shortVersionString")).toString();
+                    updater->setDisplayVersion(shortVersionString);
+                }
+                reader.skipCurrentElement();
+            }
+        }
+    }
+
+    if (reader.hasError()) {
+        error = true;
+        errorMessage = reader.errorString();
+    }
+}
+
+} // namespace updater
diff --git a/lib/updater/src/impl/appcastparser.h b/lib/updater/src/impl/appcastparser.h
new file mode 100644 (file)
index 0000000..b38ef0d
--- /dev/null
@@ -0,0 +1,20 @@
+#ifndef APPCASTPARSER_H
+#define APPCASTPARSER_H
+
+#include <QtCore>
+
+#include "parser.h"
+
+namespace updater {
+
+class DefaultUpdater;
+
+class AppcastParser : public Parser {
+public:
+    AppcastParser();
+    void parse(const QByteArray &bytes);
+};
+
+} // namespace updater
+
+#endif // APPCASTPARSER_H
diff --git a/lib/updater/src/impl/checker.cpp b/lib/updater/src/impl/checker.cpp
new file mode 100644 (file)
index 0000000..91428d3
--- /dev/null
@@ -0,0 +1,88 @@
+#include "checker.h"
+
+#include "appcastparser.h"
+#include "defaultupdater.h"
+#include "parser.h"
+
+#ifdef HTTP
+#include "http.h"
+#endif
+
+namespace {
+const QString updateCheckKey = "updateCheck";
+}
+
+namespace updater {
+
+Checker::Checker(DefaultUpdater *updater, QObject *parent) : QObject(parent), updater(updater) {}
+
+void Checker::check() {
+    error = false;
+    errorMessage.clear();
+
+#ifdef HTTP
+    HttpReply *reply = Http::instance().get(updater->getManifestUrl());
+    connect(reply, &HttpReply::finished, this, [this](const HttpReply &reply) {
+        if (reply.isSuccessful()) {
+            invokeParser(reply.body());
+        } else {
+            qWarning() << "Update check failed" << reply.statusCode() << reply.reasonPhrase();
+            error = true;
+            errorMessage = reply.reasonPhrase();
+            emit done();
+        }
+    });
+#else
+    QNetworkAccessManager *manager = new QNetworkAccessManager(this);
+    QNetworkRequest request;
+    request.setUrl(updater->getManifestUrl());
+    request.setRawHeader("User-Agent",
+                         (updater->getAppName() + ' ' + updater->getLocalVersion()).toUtf8());
+    connect(manager, &QNetworkAccessManager::finished, this, [this](QNetworkReply *reply) {
+        int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+        if (status >= 200 && status < 300) {
+            invokeParser(reply->readAll());
+        } else {
+            QString msg = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
+            qWarning() << "Update check failed" << status << msg;
+            error = true;
+            errorMessage = msg;
+            emit done();
+        }
+        reply->deleteLater();
+    });
+    manager->get(request);
+#endif
+} // namespace updater
+
+qint64 Checker::getLastCheck() {
+    QSettings settings;
+    return settings.value(updateCheckKey).toInt();
+}
+
+void Checker::invokeParser(const QByteArray &bytes) {
+    auto parser = updater->getParser();
+    if (!parser) {
+        AppcastParser defaultParser;
+        defaultParser.setUpdater(updater);
+        defaultParser.parse(bytes);
+    } else {
+        parser->parse(bytes);
+    }
+
+    bool versionsDontMatch =
+            !updater->getVersion().isEmpty() && updater->getVersion() != updater->getLocalVersion();
+    if (versionsDontMatch && updater->getStatus() != Updater::Status::UpdateDownloaded) {
+        if (updater->getAutomaticDownload()) {
+            updater->downloadUpdate();
+        } else {
+            updater->setStatus(Updater::Status::UpdateAvailable);
+        }
+    }
+
+    QSettings settings;
+    settings.setValue(updateCheckKey, QDateTime::currentSecsSinceEpoch());
+    emit done();
+}
+
+} // namespace updater
diff --git a/lib/updater/src/impl/checker.h b/lib/updater/src/impl/checker.h
new file mode 100644 (file)
index 0000000..c16ffa9
--- /dev/null
@@ -0,0 +1,36 @@
+#ifndef UPDATER_IMPL_CHECKER_H
+#define UPDATER_IMPL_CHECKER_H
+
+#include <QtNetwork>
+
+namespace updater {
+
+class DefaultUpdater;
+
+class Checker : public QObject {
+    Q_OBJECT
+
+public:
+    explicit Checker(DefaultUpdater *updater, QObject *parent = nullptr);
+
+    void check();
+    bool hasError() const { return error; };
+    QString getErrorMessage() const { return errorMessage; };
+
+    static qint64 getLastCheck();
+
+signals:
+    void done();
+
+private:
+    void invokeParser(const QByteArray &bytes);
+
+    DefaultUpdater *updater;
+
+    bool error = false;
+    QString errorMessage;
+};
+
+} // namespace updater
+
+#endif // UPDATER_IMPL_CHECKER_H
diff --git a/lib/updater/src/impl/defaultupdater.cpp b/lib/updater/src/impl/defaultupdater.cpp
new file mode 100644 (file)
index 0000000..5c0e9aa
--- /dev/null
@@ -0,0 +1,147 @@
+#include "defaultupdater.h"
+
+#include "checker.h"
+#include "dialog.h"
+#include "downloader.h"
+#include "installer.h"
+#include "parser.h"
+#include "runinstaller.h"
+
+namespace updater {
+
+DefaultUpdater::DefaultUpdater() {
+    checkTimer = new QTimer(this);
+    checkTimer->setInterval(checkInterval);
+    checkTimer->setTimerType(Qt::VeryCoarseTimer);
+    auto autoCheck = [this] {
+        // auto check after interval
+        qint64 lastCheck = Checker::getLastCheck();
+        int secondsSinceLastCheck = QDateTime::currentSecsSinceEpoch() - lastCheck;
+        if (secondsSinceLastCheck >= checkInterval) {
+            auto checker = new Checker(this);
+            connect(checker, &Checker::done, this, [this, checker] {
+                if (!getAutomaticDownload() && getStatus() != Updater::Status::UpToDate) {
+                    showDialog();
+                }
+                checker->deleteLater();
+            });
+            checker->check();
+        }
+    };
+    connect(checkTimer, &QTimer::timeout, this, autoCheck);
+    QTimer::singleShot(5000, autoCheck);
+    checkTimer->start();
+}
+
+void DefaultUpdater::setCheckInterval(const qint64 &value) {
+    checkInterval = value;
+    checkTimer->setInterval(checkInterval);
+}
+
+void DefaultUpdater::setInstaller(Installer *value) {
+    installer = value;
+    installer->setUpdater(this);
+}
+
+void DefaultUpdater::setParser(Parser *value) {
+    parser = value;
+    parser->setUpdater(this);
+}
+
+void DefaultUpdater::checkAndShowUI() {
+    auto checker = new Checker(this);
+    connect(checker, &Checker::done, this, [this, checker] {
+        if (getStatus() == Updater::Status::UpToDate) {
+            QMessageBox msgBox(qApp->activeWindow());
+            msgBox.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+            msgBox.setWindowModality(Qt::WindowModal);
+            msgBox.setIconPixmap(getIcon().pixmap(64, 64));
+            msgBox.setText(tr("There are currently no updates available."));
+            msgBox.exec();
+        } else {
+            if (getStatus() != Updater::Status::UpdateAvailable) {
+                downloadUpdate();
+            }
+            showDialog();
+        }
+
+        checker->deleteLater();
+    });
+    checker->check();
+}
+
+void DefaultUpdater::checkAndMaybeShowUI() {
+    auto checker = new Checker(this);
+    connect(checker, &Checker::done, this, [this, checker] {
+        if (getStatus() != Updater::Status::UpToDate) {
+            showDialog();
+        }
+        checker->deleteLater();
+    });
+    checker->check();
+}
+
+void DefaultUpdater::checkWithoutUI() {
+    auto checker = new Checker(this);
+    connect(checker, &Checker::done, this, [checker] { checker->deleteLater(); });
+    checker->check();
+}
+
+void DefaultUpdater::update() {
+    if (!installer) {
+        installer = new RunInstaller();
+        installer->setUpdater(this);
+    }
+    connect(installer, &Installer::error, this, [](auto message) { qWarning() << message; });
+    installer->start(downloadedFilename);
+}
+
+Downloader *DefaultUpdater::downloadUpdate() {
+    if (downloader) return downloader;
+    if (!downloadedFilename.isEmpty()) {
+        qDebug() << "Update already downloded";
+    }
+
+    setStatus(Updater::Status::DownloadingUpdate);
+
+    downloader = new Downloader();
+    connect(downloader, &Downloader::fileReady, [this](auto filename) {
+        downloader->deleteLater();
+        downloader = nullptr;
+        downloadedFilename = filename;
+
+        if (getImmediateInstallAndRelaunch()) {
+            update();
+            qApp->quit();
+        } else {
+            connect(qApp, &QCoreApplication::aboutToQuit, this, [this] { update(); });
+        }
+
+        setStatus(Updater::Status::UpdateDownloaded);
+    });
+
+    connect(downloader, &Downloader::error, [this](auto message) {
+        qWarning() << message;
+        downloader->deleteLater();
+        downloader = nullptr;
+        downloadedFilename.clear();
+        setStatus(Updater::Status::UpdateDownloadFailed);
+    });
+
+    connect(downloader, &Downloader::progress,
+            [](int percent) { qDebug() << QString("Downloading update %1%").arg(percent); });
+
+    downloader->download(downloadUrl);
+
+    return downloader;
+}
+
+void DefaultUpdater::showDialog() {
+    if (!dialog) {
+        dialog = new Dialog(this, qApp->activeWindow());
+        connect(dialog, &QWidget::destroyed, this, [this] { dialog = nullptr; });
+    }
+    dialog->show();
+}
+
+} // namespace updater
diff --git a/lib/updater/src/impl/defaultupdater.h b/lib/updater/src/impl/defaultupdater.h
new file mode 100644 (file)
index 0000000..03314ea
--- /dev/null
@@ -0,0 +1,76 @@
+#ifndef UPDATER_IMPL_UPDATER_H
+#define UPDATER_IMPL_UPDATER_H
+
+#include "updater.h"
+
+namespace updater {
+
+class Downloader;
+class Dialog;
+class Installer;
+class Parser;
+
+class DefaultUpdater : public ::Updater {
+    Q_OBJECT
+
+public:
+    DefaultUpdater();
+
+    QString getAppName() const { return appName; }
+    void setAppName(const QString &value) { appName = value; }
+
+    QIcon getIcon() const { return icon; }
+    void setIcon(const QIcon &value) { icon = value; }
+
+    QString getLocalVersion() const { return localVersion; }
+    void setLocalVersion(const QString &value) { localVersion = value; }
+
+    QUrl getManifestUrl() const { return manifestUrl; }
+    void setManifestUrl(const QUrl &value) { manifestUrl = value; }
+
+    void setCheckInterval(const qint64 &value);
+
+    /// A fixed download url. In alternative, the Parser impl can set the download url from the
+    /// manifest
+    void setDownloadUrl(const QUrl &value) { downloadUrl = value; }
+
+    void setInstaller(Installer *value);
+
+    Parser *getParser() const { return parser; }
+    void setParser(Parser *value);
+
+    Downloader *downloadUpdate();
+    Downloader *getDownloader() const { return downloader; }
+
+    void checkAndShowUI();
+    void checkAndMaybeShowUI();
+    void checkWithoutUI();
+
+protected slots:
+    void update();
+
+private:
+    void showDialog();
+
+    QString appName = QGuiApplication::applicationDisplayName();
+    QIcon icon = QGuiApplication::windowIcon();
+    QString localVersion = QCoreApplication::applicationVersion();
+
+    QUrl manifestUrl;
+    qint64 checkInterval = 86400;
+
+    Downloader *downloader = nullptr;
+    QUrl downloadUrl;
+    QString downloadedFilename;
+
+    Installer *installer = nullptr;
+    Parser *parser = nullptr;
+
+    Dialog *dialog = nullptr;
+
+    QTimer *checkTimer;
+};
+
+} // namespace updater
+
+#endif // UPDATER_IMPL_UPDATER_H
diff --git a/lib/updater/src/impl/dialog.cpp b/lib/updater/src/impl/dialog.cpp
new file mode 100644 (file)
index 0000000..b860373
--- /dev/null
@@ -0,0 +1,153 @@
+#include "dialog.h"
+
+#include "downloader.h"
+
+namespace {
+static const int padding = 15;
+} // namespace
+
+namespace updater {
+
+DownloadWidget::DownloadWidget(const QString &message, QDialog *parent)
+    : QWidget(parent) {
+    QBoxLayout *layout = new QVBoxLayout(this);
+    layout->setMargin(0);
+    layout->setSpacing(padding);
+
+    QLabel *msgLabel = new QLabel(message);
+    QFont boldFont = msgLabel->font();
+    boldFont.setBold(true);
+    msgLabel->setFont(boldFont);
+    layout->addWidget(msgLabel);
+
+    progressBar = new QProgressBar();
+    layout->addWidget(progressBar);
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
+
+    QPushButton *cancelButton = buttonBox->addButton(QDialogButtonBox::Cancel);
+    connect(cancelButton, &QPushButton::clicked, this, [parent] { parent->reject(); });
+
+    QPushButton *closeButton = buttonBox->addButton(QDialogButtonBox::Ok);
+    closeButton->setDefault(true);
+    closeButton->setFocus();
+    closeButton->connect(closeButton, &QPushButton::clicked, this, [parent] { parent->accept(); });
+
+    layout->addWidget(buttonBox);
+
+    errorLabel = new QLabel();
+    errorLabel->hide();
+    layout->addWidget(errorLabel);
+}
+
+void DownloadWidget::setErrorMessage(const QString &message) {
+    errorLabel->setText(message);
+    errorLabel->show();
+}
+
+Dialog::Dialog(DefaultUpdater *updater, QWidget *parent)
+    : QDialog(parent), updater(updater), downloadWidget(nullptr) {
+    setWindowModality(Qt::WindowModal);
+    setAttribute(Qt::WA_DeleteOnClose);
+    setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+
+    QBoxLayout *hLayout = new QHBoxLayout(this);
+    hLayout->setSizeConstraint(QLayout::SetFixedSize);
+    hLayout->setMargin(padding * 2);
+    hLayout->setSpacing(padding);
+
+    if (!updater->getIcon().isNull()) {
+        QLabel *logo = new QLabel();
+        logo->setPixmap(updater->getIcon().pixmap(64, 64));
+        hLayout->addWidget(logo, 0, Qt::AlignTop);
+    }
+
+    stackedLayout = new QStackedLayout();
+    hLayout->addLayout(stackedLayout, 1);
+
+    auto onStatusChange = [this](Updater::Status status) {
+        QWidget *w = nullptr;
+        switch (status) {
+        case Updater::Status::UpToDate:
+            w = new QLabel(tr("You already have the latest version"));
+            break;
+        case Updater::Status::UpdateAvailable:
+            w = messageWidget();
+            break;
+
+        case Updater::Status::DownloadingUpdate:
+        case Updater::Status::UpdateDownloaded:
+        case Updater::Status::UpdateDownloadFailed:
+            w = setupDownloadWidget();
+            break;
+        }
+        if (w) showWidget(w);
+    };
+    connect(updater, &Updater::statusChanged, this, onStatusChange);
+    onStatusChange(updater->getStatus());
+}
+
+QWidget *Dialog::setupDownloadWidget() {
+    if (!downloadWidget) {
+        QString message =
+                tr("Downloading %1 %2...").arg(updater->getAppName(), updater->getVersion());
+        downloadWidget = new DownloadWidget(message, this);
+    }
+
+    Downloader *downloader = updater->getDownloader();
+    if (downloader) {
+        connect(downloader, &Downloader::progress, downloadWidget, &DownloadWidget::setProgress);
+        connect(downloader, &Downloader::error, downloadWidget, &DownloadWidget::setErrorMessage);
+        connect(this, &QDialog::rejected, downloader, &Downloader::stop);
+    }
+
+    return downloadWidget;
+}
+
+void Dialog::showWidget(QWidget *widget) {
+    QWidget *currentWidget = stackedLayout->currentWidget();
+    if (currentWidget) {
+        stackedLayout->removeWidget(currentWidget);
+        currentWidget->deleteLater();
+    }
+    stackedLayout->addWidget(widget);
+}
+
+QWidget *Dialog::messageWidget() {
+    QWidget *w = new QWidget();
+
+    QFormLayout *layout = new QFormLayout(w);
+    layout->setMargin(0);
+    layout->setSpacing(padding);
+
+    QLabel *titleLabel =
+            new QLabel(tr("A new version of %1 is available!").arg(updater->getAppName()));
+    QFont boldFont = titleLabel->font();
+    boldFont.setBold(true);
+    titleLabel->setFont(boldFont);
+    layout->addWidget(titleLabel);
+
+    QLabel *label = new QLabel(
+            tr("%1 %2 is now available. You have %3.")
+                    .arg(updater->getAppName(), updater->getVersion(), updater->getLocalVersion()));
+    layout->addWidget(label);
+
+    label = new QLabel(tr("Would you like to download it now?"));
+    layout->addWidget(label);
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox();
+
+    QPushButton *laterButton =
+            buttonBox->addButton(tr("Remind Me Later"), QDialogButtonBox::RejectRole);
+    connect(laterButton, &QPushButton::clicked, this, &QDialog::close);
+
+    QPushButton *updateButton =
+            buttonBox->addButton(tr("Download Update"), QDialogButtonBox::AcceptRole);
+    connect(updateButton, &QPushButton::clicked, updater, &DefaultUpdater::downloadUpdate);
+
+    layout->addWidget(buttonBox);
+
+    return w;
+}
+
+} // namespace updater
diff --git a/lib/updater/src/impl/dialog.h b/lib/updater/src/impl/dialog.h
new file mode 100644 (file)
index 0000000..4652e3c
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef UPDATER_IMPL_DIALOG_H
+#define UPDATER_IMPL_DIALOG_H
+
+#include <QtNetwork>
+#include <QtWidgets>
+
+#include "defaultupdater.h"
+
+namespace updater {
+
+class DownloadWidget : public QWidget {
+    Q_OBJECT
+
+public:
+    DownloadWidget(const QString &message, QDialog *parent = 0);
+
+public slots:
+    void setProgress(int percent) { progressBar->setValue(percent); }
+    void setErrorMessage(const QString &message);
+
+private:
+    QProgressBar *progressBar;
+    QLabel *errorLabel;
+};
+
+class Dialog : public QDialog {
+    Q_OBJECT
+
+public:
+    Dialog(DefaultUpdater *updater, QWidget *parent);
+
+private:
+    void showWidget(QWidget *widget);
+    QWidget *setupDownloadWidget();
+    QWidget *messageWidget();
+
+    DefaultUpdater *updater;
+    QStackedLayout *stackedLayout;
+    DownloadWidget *downloadWidget;
+};
+
+} // namespace updater
+
+#endif // UPDATER_IMPL_DIALOG_H
diff --git a/lib/updater/src/impl/downloader.cpp b/lib/updater/src/impl/downloader.cpp
new file mode 100644 (file)
index 0000000..808ec10
--- /dev/null
@@ -0,0 +1,88 @@
+#include "downloader.h"
+
+#include <QDesktopServices>
+
+namespace updater {
+
+void Downloader::download(const QUrl &url) {
+    const QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
+    QString filename = tempDir + "/" +
+                       QByteArray::number(QRandomGenerator::global()->generate()).toHex() +
+                       url.fileName();
+    file.setFileName(filename);
+
+    qDebug() << "Downloading" << url << "to" << filename;
+    HttpRequest req;
+    req.url = url;
+    reply = Http::instance().networkReply(req);
+
+    connect(reply, &QNetworkReply::readyRead, this, [this] {
+        if (!reply) return;
+
+        if (!file.isOpen()) {
+            if (!file.open(QIODevice::ReadWrite)) {
+                emit error(QString("Error opening file: %1").arg(file.errorString()));
+                reply->disconnect();
+                reply->abort();
+                reply->deleteLater();
+                reply = nullptr;
+                return;
+            }
+        }
+        if (-1 == file.write(reply->readAll())) {
+            emit error(file.errorString());
+            reply->disconnect();
+            reply->abort();
+            reply->deleteLater();
+            reply = nullptr;
+        }
+    });
+
+    connect(reply,
+            static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(
+                    &QNetworkReply::error),
+            this, [this] {
+                emit error(reply->errorString());
+                reply->disconnect();
+                reply->abort();
+                reply->deleteLater();
+                reply = nullptr;
+            });
+
+    connect(reply, &QNetworkReply::downloadProgress, this,
+            [this](qint64 bytesReceived, qint64 bytesTotal) {
+                if (bytesTotal <= 0) return;
+                int percent = bytesReceived * 100 / bytesTotal;
+                emit progress(percent);
+            });
+
+    connect(reply, &QNetworkReply::finished, this, [this] {
+        if (!reply) return;
+        int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+        if (status != 200) {
+            QString message =
+                    reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
+            if (message.isEmpty()) message = reply->errorString();
+            emit error(message);
+            return;
+        }
+
+        file.close();
+
+        reply->deleteLater();
+        reply = nullptr;
+
+        emit fileReady(file.fileName());
+    });
+}
+
+void Downloader::stop() {
+    if (!reply) return;
+    reply->disconnect();
+    reply->abort();
+    reply->deleteLater();
+    reply = nullptr;
+    emit error("Canceled");
+}
+
+} // namespace updater
diff --git a/lib/updater/src/impl/downloader.h b/lib/updater/src/impl/downloader.h
new file mode 100644 (file)
index 0000000..70602d1
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef UPDATER_IMPL_DOWNLOADER_H
+#define UPDATER_IMPL_DOWNLOADER_H
+
+#include <QtNetwork>
+
+#include "http.h"
+
+namespace updater {
+
+class Downloader : public QObject {
+    Q_OBJECT
+
+public:
+    void download(const QUrl &url);
+    void stop();
+
+signals:
+    void progress(int percent);
+    void error(const QString &message);
+    void fileReady(const QString &filename);
+
+private:
+    QNetworkReply *reply = nullptr;
+    QFile file;
+};
+
+} // namespace updater
+
+#endif // UPDATER_IMPL_DOWNLOADER_H
diff --git a/lib/updater/src/impl/installer.h b/lib/updater/src/impl/installer.h
new file mode 100644 (file)
index 0000000..27c9e35
--- /dev/null
@@ -0,0 +1,32 @@
+#ifndef UPDATER_IMPL_INSTALLER_H
+#define UPDATER_IMPL_INSTALLER_H
+
+#include <QtCore>
+
+namespace updater {
+
+class DefaultUpdater;
+
+/**
+ * Implement this interface to create your own installer mechanism
+ * E.g. Unzip and copy files, payload integrity checking, validation, etc.
+ */
+class Installer : public QObject {
+    Q_OBJECT
+
+public:
+    Installer(){};
+    void setUpdater(DefaultUpdater *value) { updater = value; }
+
+    virtual void start(const QString &filename) = 0;
+
+signals:
+    void error(const QString &message);
+
+protected:
+    DefaultUpdater *updater = nullptr;
+};
+
+} // namespace updater
+
+#endif // UPDATER_IMPL_INSTALLER_H
diff --git a/lib/updater/src/impl/parser.h b/lib/updater/src/impl/parser.h
new file mode 100644 (file)
index 0000000..21d6797
--- /dev/null
@@ -0,0 +1,31 @@
+#ifndef UPDATER_IMPL_PARSER_H
+#define UPDATER_IMPL_PARSER_H
+
+#include <QtCore>
+
+namespace updater {
+
+class DefaultUpdater;
+
+/**
+ * Implement this interface to parse your custom manifest format
+ */
+class Parser {
+public:
+    /// Parse your manifest and set relevant Updater properties
+    virtual void parse(const QByteArray &bytes) = 0;
+
+    void setUpdater(DefaultUpdater *value) { updater = value; }
+    bool hasError() const { return error; };
+    QString getErrorMessage() const { return errorMessage; };
+
+protected:
+    Parser(){};
+    DefaultUpdater *updater = nullptr;
+    bool error = false;
+    QString errorMessage;
+};
+
+} // namespace updater
+
+#endif // UPDATER_IMPL_PARSER_H
diff --git a/lib/updater/src/impl/runinstaller.cpp b/lib/updater/src/impl/runinstaller.cpp
new file mode 100644 (file)
index 0000000..67aa7f2
--- /dev/null
@@ -0,0 +1,22 @@
+#include "runinstaller.h"
+
+#include <QDesktopServices>
+
+namespace updater {
+
+RunInstaller::RunInstaller() : Installer() {}
+
+void RunInstaller::start(const QString &filename) {
+    if (arguments.isEmpty()) {
+        if (!QDesktopServices::openUrl(QUrl("file:///" + filename)))
+            emit error("Cannot start update");
+    } else {
+        QProcess *process = new QProcess(this);
+        QObject::connect(process, &QProcess::errorOccurred, this, [this](auto error) {
+            this->emit error("Update error: " + QVariant::fromValue(error).toString());
+        });
+        process->startDetached(filename, arguments);
+    }
+}
+
+} // namespace updater
diff --git a/lib/updater/src/impl/runinstaller.h b/lib/updater/src/impl/runinstaller.h
new file mode 100644 (file)
index 0000000..fbcc7ac
--- /dev/null
@@ -0,0 +1,27 @@
+#ifndef UPDATER_IMPL_RUNINSTALLER_H
+#define UPDATER_IMPL_RUNINSTALLER_H
+
+#include <QObject>
+
+#include "installer.h"
+
+namespace updater {
+
+/**
+ * Installer implementation that simply runs the downloaded update
+ */
+class RunInstaller : public Installer {
+    Q_OBJECT
+
+public:
+    RunInstaller();
+    void setArguments(const QStringList &value) { arguments = value; };
+    void start(const QString &filename);
+
+private:
+    QStringList arguments;
+};
+
+} // namespace updater
+
+#endif // UPDATER_IMPL_RUNINSTALLER_H
diff --git a/lib/updater/src/impl/simplexmlparser.cpp b/lib/updater/src/impl/simplexmlparser.cpp
new file mode 100644 (file)
index 0000000..39d073a
--- /dev/null
@@ -0,0 +1,40 @@
+#include "simplexmlparser.h"
+
+#include "defaultupdater.h"
+
+namespace updater {
+
+SimpleXmlParser::SimpleXmlParser() {}
+
+void SimpleXmlParser::parse(const QByteArray &bytes) {
+    error = false;
+    errorMessage.clear();
+
+    QXmlStreamReader reader;
+    reader.addData(bytes);
+    while (!reader.atEnd()) {
+        reader.readNext();
+        if (reader.isStartElement()) {
+            if (reader.name() == rootElementName) {
+                while (!reader.atEnd()) {
+                    reader.readNext();
+                    if (reader.isStartElement() && reader.name() == versionElementName) {
+                        updater->setVersion(reader.readElementText());
+                    } else if (reader.isStartElement() && reader.name() == urlElementName) {
+                        QString url = reader.readElementText();
+                        for (const auto &arg : qAsConst(urlArguments)) {
+                            url = url.arg(arg);
+                        }
+                        updater->setDownloadUrl(url);
+                    }
+                }
+            }
+        }
+    }
+    if (reader.hasError()) {
+        error = true;
+        errorMessage = reader.errorString();
+    }
+}
+
+} // namespace updater
diff --git a/lib/updater/src/impl/simplexmlparser.h b/lib/updater/src/impl/simplexmlparser.h
new file mode 100644 (file)
index 0000000..09df3d4
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef SIMPLEXMLPARSER_H
+#define SIMPLEXMLPARSER_H
+
+#include <QtCore>
+
+#include "parser.h"
+
+namespace updater {
+
+class DefaultUpdater;
+
+/**
+ * Format example:
+ *
+ * <release><version>3.4.2</version></release>
+ *
+ * Or
+ *
+ * <release>
+ * <version>3.4.2</version>
+ * <url>https://mydomain/mypayload.%1</url>
+ * </release>
+ */
+class SimpleXmlParser : public Parser {
+
+public:
+    explicit SimpleXmlParser();
+    void parse(const QByteArray &bytes);
+
+    void setRootElementName(const QString &value) { rootElementName = value; }
+    void setVersionElementName(const QString &value) { versionElementName = value; }
+    void setUrlElementName(const QString &value) { urlElementName = value; }
+    void setUrlArguments(const QStringList &value) { urlArguments = value; }
+
+private:
+    QString rootElementName = "release";
+    QString versionElementName = "version";
+    QString urlElementName = "url";
+    QStringList urlArguments;
+};
+
+} // namespace updater
+
+#endif // SIMPLEXMLPARSER_H
diff --git a/lib/updater/src/sparkle/sparkleupdater.h b/lib/updater/src/sparkle/sparkleupdater.h
new file mode 100644 (file)
index 0000000..0f416a9
--- /dev/null
@@ -0,0 +1,27 @@
+#ifndef SPARKLE_UPDATER_H
+#define SPARKLE_UPDATER_H
+
+#include "updater.h"
+
+namespace updater {
+
+class SparkleUpdater : public ::Updater {
+    Q_OBJECT
+
+public:
+    SparkleUpdater();
+
+    bool getAutomaticDownload() const;
+    void setAutomaticDownload(bool value);
+
+    void checkAndShowUI();
+    void checkAndMaybeShowUI();
+    void checkWithoutUI();
+
+protected slots:
+    void update();
+};
+
+} // namespace updater
+
+#endif // SPARKLE_UPDATER_H
diff --git a/lib/updater/src/sparkle/sparkleupdater.mm b/lib/updater/src/sparkle/sparkleupdater.mm
new file mode 100644 (file)
index 0000000..61fc81c
--- /dev/null
@@ -0,0 +1,114 @@
+#include "sparkleupdater.h"
+
+#import <SUAppcastItem.h>
+#import <SUUpdater.h>
+#import <SUUpdaterDelegate.h>
+
+@interface SparkleDelegate : NSObject <SUUpdaterDelegate> {
+}
+@end
+
+@implementation SparkleDelegate {
+    Updater *u;
+}
+
+- (void)setUpdater:(Updater *)v {
+    u = v;
+}
+
+- (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)item {
+    qDebug() << item.versionString;
+    Q_UNUSED(updater)
+    u->setVersion(QString::fromNSString(item.displayVersionString));
+    u->setStatus(Updater::Status::UpdateAvailable);
+    // [updater installUpdatesIfAvailable];
+}
+
+- (void)updaterDidNotFindUpdate:(SUUpdater *)updater {
+    Q_UNUSED(updater)
+    u->setStatus(Updater::Status::UpToDate);
+}
+
+- (void)updater:(SUUpdater *)updater
+        willDownloadUpdate:(SUAppcastItem *)item
+               withRequest:(NSMutableURLRequest *)request {
+    qDebug() << item.versionString << request;
+    Q_UNUSED(updater)
+    u->setStatus(Updater::Status::DownloadingUpdate);
+}
+
+- (void)updater:(SUUpdater *)updater didDownloadUpdate:(SUAppcastItem *)item {
+    qDebug() << item.versionString;
+    Q_UNUSED(updater)
+    u->setStatus(Updater::Status::UpdateDownloaded);
+}
+
+- (void)userDidCancelDownload:(SUUpdater *)updater {
+    Q_UNUSED(updater)
+    u->setStatus(Updater::Status::UpdateAvailable);
+}
+
+- (void)updater:(SUUpdater *)updater
+        failedToDownloadUpdate:(SUAppcastItem *)item
+                         error:(NSError *)error {
+    qDebug() << error;
+    u->setStatus(Updater::Status::UpdateDownloadFailed);
+}
+
+- (void)updater:(SUUpdater *)updater
+                willInstallUpdateOnQuit:(SUAppcastItem *)item
+        immediateInstallationInvocation:(NSInvocation *)invocation {
+    Q_UNUSED(updater)
+    qDebug() << u->getImmediateInstallAndRelaunch();
+    if (u->getImmediateInstallAndRelaunch()) [invocation invoke];
+}
+
+- (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)item {
+    qDebug() << item.versionString;
+}
+
+- (void)updaterWillRelaunchApplication:(SUUpdater *)updater {
+    qDebug() << updater;
+}
+
+- (void)updater:(SUUpdater *)updater didAbortWithError:(NSError *)error {
+    qDebug() << error;
+}
+
+@end
+
+namespace updater {
+
+SparkleUpdater::SparkleUpdater() {
+    SparkleDelegate *delegate = [[SparkleDelegate alloc] init];
+    [delegate setUpdater:this];
+    [[SUUpdater sharedUpdater] setDelegate:delegate];
+    [SUUpdater sharedUpdater].automaticallyChecksForUpdates = YES;
+    [SUUpdater sharedUpdater].automaticallyDownloadsUpdates = YES;
+}
+
+bool SparkleUpdater::getAutomaticDownload() const {
+    return [SUUpdater sharedUpdater].automaticallyDownloadsUpdates;
+}
+
+void SparkleUpdater::setAutomaticDownload(bool value) {
+    [SUUpdater sharedUpdater].automaticallyDownloadsUpdates = value;
+}
+
+void SparkleUpdater::checkAndShowUI() {
+    [[SUUpdater sharedUpdater] checkForUpdates:nil];
+}
+
+void SparkleUpdater::checkAndMaybeShowUI() {
+    [[SUUpdater sharedUpdater] checkForUpdatesInBackground];
+}
+
+void SparkleUpdater::checkWithoutUI() {
+    [[SUUpdater sharedUpdater] checkForUpdateInformation];
+}
+
+void SparkleUpdater::update() {
+    [[SUUpdater sharedUpdater] installUpdatesIfAvailable];
+}
+
+} // namespace updater
diff --git a/lib/updater/src/updater.cpp b/lib/updater/src/updater.cpp
new file mode 100644 (file)
index 0000000..0f63bf5
--- /dev/null
@@ -0,0 +1,133 @@
+#include "updater.h"
+
+namespace {
+Updater *sharedInstance = nullptr;
+}
+
+void Updater::setInstance(Updater *value) {
+    sharedInstance = value;
+}
+
+Updater &Updater::instance() {
+    return *sharedInstance;
+}
+
+Updater::Updater(QObject *parent) : QObject(parent) {}
+
+QAction *Updater::getAction() {
+    if (!action) {
+        action = new QAction(this);
+        connect(action, &QWidget::destroyed, this, [this] { action = nullptr; });
+        action->setMenuRole(QAction::ApplicationSpecificRole);
+        connect(action, &QAction::triggered, this, &Updater::onUserAction);
+        auto onStatusChange = [this](Updater::Status status) {
+            QString v = displayVersion.isEmpty() ? version : displayVersion;
+            QString t;
+            switch (status) {
+            case Updater::Status::UpToDate:
+                t = tr("Check for Updates...");
+                break;
+            case Updater::Status::UpdateAvailable:
+                t = tr("Version %1 is available...").arg(v);
+                break;
+            case Updater::Status::DownloadingUpdate:
+                t = tr("Downloading version %1...").arg(v);
+                break;
+            case Updater::Status::UpdateDownloaded:
+                t = tr("Restart to Update");
+                break;
+            case Updater::Status::UpdateDownloadFailed:
+                t = tr("Version %1 download failed").arg(v);
+                break;
+            }
+            action->setText(t);
+        };
+        connect(this, &Updater::statusChanged, this, onStatusChange);
+        onStatusChange(status);
+    }
+    return action;
+}
+
+QPushButton *Updater::getButton() {
+    if (!button) {
+        button = new QPushButton();
+        connect(button, &QWidget::destroyed, this, [this] { button = nullptr; });
+        connect(button, &QPushButton::clicked, this, &Updater::onUserAction);
+        auto onStatusChange = [this](Updater::Status status) {
+            QString t;
+            bool visible = true;
+            switch (status) {
+            case Updater::Status::UpToDate:
+                t = tr("Check for Updates");
+                break;
+            case Updater::Status::UpdateAvailable:
+                t = tr("Download Update");
+                break;
+            case Updater::Status::DownloadingUpdate:
+                t = tr("Downloading update...");
+                visible = false;
+                break;
+            case Updater::Status::UpdateDownloaded:
+                t = tr("Restart to Update");
+                break;
+            case Updater::Status::UpdateDownloadFailed:
+                t = tr("Retry Update Download");
+                break;
+            }
+            button->setText(t);
+            button->setVisible(visible);
+        };
+        connect(this, &Updater::statusChanged, this, onStatusChange);
+        onStatusChange(status);
+    }
+    return button;
+}
+
+QLabel *Updater::getLabel() {
+    if (!label) {
+        label = new QLabel();
+        connect(label, &QWidget::destroyed, this, [this] { label = nullptr; });
+        auto onStatusChange = [this](Updater::Status status) {
+            QString v = displayVersion.isEmpty() ? version : displayVersion;
+            QString t;
+            switch (status) {
+            case Updater::Status::UpToDate:
+                t = tr("You have the latest version.");
+                break;
+            case Updater::Status::UpdateAvailable:
+                t = tr("Version %1 is available.").arg(v);
+                break;
+            case Updater::Status::DownloadingUpdate:
+                t = tr("Downloading update...");
+                break;
+            case Updater::Status::UpdateDownloaded:
+                t = tr("An update has been downloaded and is ready to be installed.");
+                break;
+            case Updater::Status::UpdateDownloadFailed:
+                t = tr("Version %1 download failed").arg(v);
+                break;
+            }
+            label->setText(t);
+        };
+        connect(this, &Updater::statusChanged, this, onStatusChange);
+        onStatusChange(status);
+    }
+    return label;
+}
+
+void Updater::setStatus(Status v) {
+    if (status != v) {
+        status = v;
+        emit statusChanged(status);
+    }
+}
+
+void Updater::onUserAction() {
+    if (status == Updater::Status::UpdateDownloaded) {
+        // update will be installed on quit
+        qApp->quit();
+        return;
+    } else {
+        checkAndShowUI();
+    }
+}
diff --git a/lib/updater/src/updater.h b/lib/updater/src/updater.h
new file mode 100644 (file)
index 0000000..457b1cc
--- /dev/null
@@ -0,0 +1,73 @@
+#ifndef UPDATER_H
+#define UPDATER_H
+
+#include <QtWidgets>
+
+
+class Updater : public QObject {
+    Q_OBJECT
+
+public:
+    /// Set a shared Updater instance
+    static void setInstance(Updater *value);
+
+    /// Get a shared updater instance, null by default
+    static Updater &instance();
+
+    Updater(QObject *parent = nullptr);
+
+    QAction *getAction();
+    QPushButton *getButton();
+    QLabel *getLabel();
+
+    enum class Status {
+        UpToDate,
+        UpdateAvailable,
+        DownloadingUpdate,
+        UpdateDownloaded,
+        UpdateDownloadFailed
+    };
+    Q_ENUM(Status)
+    Status getStatus() const { return status; }
+    // This should be protected
+    void setStatus(Status v);
+
+    QString getVersion() const { return version; }
+    // This should be protected
+    void setVersion(const QString &value) { version = value; }
+
+    QString getDisplayVersion() const { return displayVersion; }
+    // This should be protected
+    void setDisplayVersion(const QString &value) { displayVersion = value; }
+
+    virtual bool getImmediateInstallAndRelaunch() const { return immediateInstallAndRelaunch; }
+    virtual void setImmediateInstallAndRelaunch(bool value) { immediateInstallAndRelaunch = value; }
+
+    virtual bool getAutomaticDownload() const { return automaticDownload; }
+    virtual void setAutomaticDownload(bool value) { automaticDownload = value; }
+
+    virtual void checkAndShowUI() = 0;
+    virtual void checkAndMaybeShowUI() = 0;
+    virtual void checkWithoutUI() = 0;
+
+protected slots:
+    virtual void update() = 0;
+    virtual void onUserAction();
+
+signals:
+    void statusChanged(Status status);
+    void actionTextChanged(const QString &actionText);
+
+private:
+    QAction *action = nullptr;
+    QPushButton *button = nullptr;
+    QLabel *label = nullptr;
+
+    bool automaticDownload = true;
+    bool immediateInstallAndRelaunch = false;
+    Status status = Status::UpToDate;
+    QString version;
+    QString displayVersion;
+};
+
+#endif // UPDATER_H
diff --git a/lib/updater/updater.pri b/lib/updater/updater.pri
new file mode 100644 (file)
index 0000000..5bd82b0
--- /dev/null
@@ -0,0 +1,42 @@
+DEFINES *= UPDATER
+
+INCLUDEPATH += $$PWD/src
+DEPENDPATH += $$PWD/src
+
+HEADERS += $$PWD/src/updater.h
+SOURCES += $$PWD/src/updater.cpp
+
+macx:!contains(DEFINES, UPDATER_NO_SPARKLE) {
+    DEFINES += UPDATER_SPARKLE
+    INCLUDEPATH += $$PWD/src/sparkle
+    DEPENDPATH += $$PWD/src/sparkle
+    LIBS += -F/Library/Frameworks -framework Sparkle
+    INCLUDEPATH += /Library/Frameworks/Sparkle.framework/Headers
+    HEADERS += $$PWD/src/sparkle/sparkleupdater.h
+    OBJECTIVE_SOURCES += $$PWD/src/sparkle/sparkleupdater.mm
+} else {
+    DEFINES += UPDATER_DEFAULT
+    QT *= network
+    INCLUDEPATH += $$PWD/src/impl
+    DEPENDPATH += $$PWD/src/impl
+
+    HEADERS += \
+    $$PWD/src/impl/checker.h \
+    $$PWD/src/impl/defaultupdater.h \
+    $$PWD/src/impl/dialog.h \
+    $$PWD/src/impl/downloader.h \
+    $$PWD/src/impl/installer.h \
+    $$PWD/src/impl/parser.h \
+    $$PWD/src/impl/runinstaller.h \
+    $$PWD/src/impl/simplexmlparser.h \
+    $$PWD/src/impl/appcastparser.h
+
+    SOURCES +=  \
+    $$PWD/src/impl/checker.cpp \
+    $$PWD/src/impl/defaultupdater.cpp \
+    $$PWD/src/impl/dialog.cpp \
+    $$PWD/src/impl/downloader.cpp \
+    $$PWD/src/impl/runinstaller.cpp \
+    $$PWD/src/impl/simplexmlparser.cpp \
+    $$PWD/src/impl/appcastparser.cpp
+}
diff --git a/lib/updater/updater.pro b/lib/updater/updater.pro
new file mode 100644 (file)
index 0000000..b33e016
--- /dev/null
@@ -0,0 +1,2 @@
+TEMPLATE = lib
+include(updater.pri)
index fc2a950467655eb4da78bee1932aa5f394b9847f..033e961d96f1a71650a21bd050aa0025e8c9a345 100644 (file)
         <source>Please license %1</source>
         <translation>الرجاء قم بترخيص %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>أنتهت مدة النسخة التجريبية.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>تسمح لك النسخة الكاملة بمشاهدة مقاطع الفيديو دون مقاطعة.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>بدون رخصة, سوف تنتهي النسخة التجريبية من هذا البرنامج خلال 20 يوما</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>بشرائك النسخة الكاملة، سوف تقوم أيضا بدعم العمل الشاق الذي قمت به في إنشاء %1.</translation>
         <source>Subscribed to %1</source>
         <translation>تمت اضافة متابعة ل %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>الحصول على النسخة الكاملة</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index bf7f8588231d5d931ab0225fb24a9cd7d1d4eade..aa95dcda56dde1882f18d9c70c117d0e0c225425 100644 (file)
         <source>Please license %1</source>
         <translation>Obtén una llicencia de %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Esta demo caducó</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation type="unfinished"/>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Ensin una llicencia, l&apos;aplicación va caducar en %1 díes.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Al mercar la versión completa, tamién sofites el trabayu fechu na creación de %1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Consigui la versión completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index a887a34c7bc0cbfc6e478d035a6ad4c735c5030d..8e87e6d3c93d1675ef232929012920ed33e98741 100644 (file)
         <source>Please license %1</source>
         <translation>Набывайце ліцэнзію %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Тэрмін дэманстрацыі кончыўся.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Поўная версія дазволіць глядзець відэа бесперапынна.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Без ліцэнзіі праграма стане нядзейснай цераз %1 дзён.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Купляючы поўную версію, вы падтрымліваеце маю цяжкую працу па стварэнні %1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Атрымаць поўную версію</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 1a9264f1fbecc5e897bead0bb2149d4123aa9aa8..5a644e5297bbad11ee4f1913a94a85425d6df428 100644 (file)
         <source>Please license %1</source>
         <translation>Моля, закупете лиценз %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Това демо изтече</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation type="unfinished"/>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Без лиценз срока за ползване ще изтече за &amp;1 ден/дни.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Закупувайки пълната версия, също така подкрепяте труда, който съм вложил в създаването &amp;1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Пълна версия</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 64c3ecfe8f940521ad7cbacde85c655a75ec1a2e..7b9383922d913244666a81118d3ad62895f40e8f 100644 (file)
         <source>Please license %1</source>
         <translation>Si us plau, llicencia el %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Aquesta demo ha expirat.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>La versió completa permet visualitzar videos sense interrupcions.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Sense llicència, l&apos;aplicació caducarà en %1 dies</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Comprant la versió completa, també donarà suport al dur treball que he posat en la creació del %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Subscrit a %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Aconsegueix la versió completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 300c0f897196e6dbe3a9cda77bfc2146bcd699e4..f7f6315f2acf9cea31a9d6091f42a10c7da98ccf 100644 (file)
         <source>Please license %1</source>
         <translation>Si us plau, llicencia el %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Aquesta demo ha expirat.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation type="unfinished"/>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Sense llicència, l&apos;aplicació caducarà en %1 dies</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Comprant la versió completa, també donarà suport al dur treball que he posat en la creació del %1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Aconsegueix la versió completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 816e0d28db9c2e0df087feeba78ef4b03a010f60..097814008e6f64477bc4f3753f844ef9f3651736 100644 (file)
         <source>Please license %1</source>
         <translation>Sežeňte si, prosím, licenci %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Platnost demoverze vypršela.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Plná verze umožní sledovat videa bez přerušení.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Bez licence žádost vyprší za %1 dnů</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Zakoupením plné verze také podpoříte práci, která byla vložená do tvorby %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Přihlášen k odběru %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Získat plnou verzi</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index db5efdc55c2d4c4af14c6c9080b8a9e35f293600..784eb391fc462edfd0afbc3262ace57097ec119e 100644 (file)
         <source>Please license %1</source>
         <translation>Venligst licensér %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Denne prøveversion er udløbet.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Den fulde version giver dig mulighed for, at se videoer uden afbrydelser.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Dette program vil udløbe om %1 dage uden en licens.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Ved at købe den fulde version, støtter du også støtte det hårde arbejde jeg lagt i at udvikle %1.</translation>
@@ -867,6 +859,18 @@ Kopiér &amp;URL&apos;en til videostrømmen</translation>
         <source>Subscribed to %1</source>
         <translation>Abonnerer nu på %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
@@ -1094,6 +1098,18 @@ Kopiér &amp;URL&apos;en til videostrømmen</translation>
         <source>Get the full version</source>
         <translation>Hent den fulde version</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 1430c29f18692f3a6c27d8a522f787ce537e8233..f0d494e53ca85d5b47d1c4553736da2617e444b5 100644 (file)
         <source>Please license %1</source>
         <translation>Bitte lizenzieren Sie %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Diese Demo ist abgelaufen.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Die Vollversion erlaubt es Ihnen, Videos ohne Unterbrechung anzuschauen.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Ohne Lizenz läuft das Programm nur noch %1 Tage.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Mit dem Kauf der Vollversion unterstützen Sie auch die harte Arbeit, die ich in die Erstellung von %1 gesteckt habe.</translation>
         <source>Subscribed to %1</source>
         <translation>%1 abonniert</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Die Vollversion kaufen</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 330b6c0b792ee07fa17bd8d2d33160c7be1ebd58..5ee3822873d1966dc6518b4f42720933b52ffb85 100644 (file)
     </message>
     <message>
         <source>Powered by %1</source>
-        <translation type="unfinished"/>
+        <translation>Τροφοδοτούμενο από %1</translation>
     </message>
     <message>
         <source>Open-source software</source>
-        <translation type="unfinished"/>
+        <translation>Λογισμικό ανοιχτού κώδικα</translation>
     </message>
     <message>
         <source>Icon designed by %1.</source>
@@ -54,7 +54,7 @@
     <name>ActivationDialog</name>
     <message>
         <source>Enter your License Details</source>
-        <translation>Εισάγετε τις λεπτομέρειες της άδειας χρήσης</translation>
+        <translation>Î\95ιÏ\83αγάγεÏ\84ε Ï\84ιÏ\82 Î»ÎµÏ\80Ï\84ομέÏ\81ειεÏ\82 Ï\84ηÏ\82 Î¬Î´ÎµÎ¹Î±Ï\82 Ï\87Ï\81ήÏ\83ηÏ\82</translation>
     </message>
     <message>
         <source>&amp;Email:</source>
         <source>Please license %1</source>
         <translation>Παρακαλώ αποκτήστε την άδεια χρήσης του %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Αυτή η δοκιμαστική έκδοση έληξε.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Η πλήρης έκδοση σας επιτρέπει να παρακολουθήσετε βίντεο χωρίς διακοπές.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Χωρίς την άδεια χρήσης, η εφαρμογή θα λήξη σε %1 ημέρες.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Αγοράζοντας την πλήρη έκδοση, υποστηρίζετε επίσης την σκληρή δουλειά που έχω κάνει για τη δημιουργία του %1.</translation>
     </message>
     <message numerus="yes">
         <source>You have %n new video(s)</source>
-        <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+        <translation><numerusform>Έχετε %n νέο βίντεο</numerusform><numerusform>Έχετε %n νέα βίντεο</numerusform></translation>
     </message>
 </context>
 <context>
     </message>
     <message numerus="yes">
         <source>%n hour(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+        <translation><numerusform>πριν %n ώρα</numerusform><numerusform>πριν %n ώρες</numerusform></translation>
     </message>
     <message numerus="yes">
         <source>%n day(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+        <translation><numerusform>πριν %n ημέρα</numerusform><numerusform>πριν %n ημέρες</numerusform></translation>
     </message>
     <message numerus="yes">
         <source>%n month(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+        <translation><numerusform>πριν %n μήνα</numerusform><numerusform>πριν %n μήνες</numerusform></translation>
     </message>
     <message>
         <source>K</source>
         <comment>K as in Kilo, i.e. thousands</comment>
-        <translation type="unfinished"/>
+        <translation>Χ</translation>
     </message>
     <message>
         <source>M</source>
         <comment>M stands for Millions</comment>
-        <translation type="unfinished"/>
+        <translation>Ε</translation>
     </message>
     <message>
         <source>B</source>
         <comment>B stands for Billions</comment>
-        <translation type="unfinished"/>
+        <translation>Δ</translation>
     </message>
     <message>
         <source>%1 views</source>
     </message>
     <message numerus="yes">
         <source>%n week(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+        <translation><numerusform>πριν %n εβδομάδα</numerusform><numerusform>πριν %n εβδομάδες</numerusform></translation>
     </message>
 </context>
 <context>
     </message>
     <message numerus="yes">
         <source>%n Download(s)</source>
-        <translation type="unfinished"><numerusform></numerusform><numerusform></numerusform></translation>
+        <translation><numerusform>%n Λήψη</numerusform><numerusform>%n Λήψεις</numerusform></translation>
     </message>
 </context>
 <context>
     </message>
     <message>
         <source>Restricted Mode</source>
-        <translation type="unfinished"/>
+        <translation>Περιορισμένη λειτουργία</translation>
     </message>
     <message>
         <source>Hide videos that may contain inappropriate content</source>
-        <translation type="unfinished"/>
+        <translation>Απόκρυψη των βίντεο που μπορεί να περιέχουν ακατάλληλο περιεχόμενο</translation>
     </message>
     <message>
         <source>Toggle &amp;Menu Bar</source>
-        <translation type="unfinished"/>
+        <translation>Ε&amp;ναλλαγή γραμμής μενού</translation>
     </message>
     <message>
         <source>Menu</source>
-        <translation type="unfinished"/>
+        <translation>Μενού</translation>
     </message>
     <message>
         <source>&amp;Love %1? Rate it!</source>
     </message>
     <message>
         <source>You can still access the menu bar by pressing the ALT key</source>
-        <translation type="unfinished"/>
+        <translation>Μπορείτε να εμφανίσετε τη γραμμή μενού με το πλήκτρο ALT</translation>
     </message>
 </context>
 <context>
         <source>Subscribed to %1</source>
         <translation>Εγγραφή στο %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation>κανάλι</translation>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Αποκτήστε την πλήρη έκδοση</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation>Αφαίρεση</translation>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation>Χρειάζεστε ένα τηλεχειριστήριο για το %1; Δοκιμάστε το %2!</translation>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index f029b0561a289cec9c509e48555973e247199178..869b2540dcb0abcd00ba9fe28a36ba5e1fac35f2 100644 (file)
         <source>Please license %1</source>
         <translation>Please license %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>This demo has expired.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>The full version allows you to watch videos without interruptions.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Without a licence, the application will expire in %1 days.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>By purchasing the full version, you will also support the hard work I put into creating %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Subscribed to %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Get the full version</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 001805385fa65c9ed3877258ed6e81c763e02932..f42e849bde803b795d5ed5293bd59090d292ea80 100644 (file)
         <source>Please license %1</source>
         <translation>Obtenga una licencia de %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Esta versión de demostración ha caducado.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>La versión completa le permite ver vídeos sin interrupciones.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Sin una licencia, la aplicación caducará en %1 días.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Al comprar la versión completa, también apoya el trabajo realizado en la creación de %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Suscrito a %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Obtener la versión completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 5f9e59ce18fd8d3f6c536ed36d9fcf1b697e8253..8f37a26bc2ca919bd729093adb72ffbd9d100220 100644 (file)
         <source>Please license %1</source>
         <translation>Por favor autorice %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Esta prueba ha vencido.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>La versión completa le permite ver videos sin interrupciones.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Sin una licencia, la aplicación caducará en %1 días.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Al comprar la versión completa, también apoyas el duro trabajo que he puesto creado %1</translation>
         <source>Subscribed to %1</source>
         <translation>Subscripto a %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Conseguir la versión completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 5dea2135544e59dad3eca793e0353a8f9243ce58..1cb756ee3c693ecd2fcbc40fe33855fb83ab99d2 100644 (file)
         <source>Please license %1</source>
         <translation>Introduzca la licencia de %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>La versión de prueba ha caducado.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>La versión completa le permite mirar videos sin interrupciones.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>La aplicación caducará en %1 días si no tiene una licencia.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Al comprar la versión completa, estará apoyando el gran trabajo empleado en la creación de %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Subscripto a %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Obtener la versión completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 80e6566e0b05e0ced5cc05565aa952a76442f72f..506786a11b6c2f277fa66d8b1e0ffaafd4753387 100644 (file)
         <source>Please license %1</source>
         <translation>Por favor licencia %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Esta demostración ha expirado</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>La versión completa te permite ver vídeos sin interrupciones</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Sin una licencia, la aplicación expirará en %1 dias.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Comprando la versión completa, permitirás el desarrollo y el trabajo duro que he puesto creando %1</translation>
         <source>Subscribed to %1</source>
         <translation>Suscrito a %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Obtener la versión completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 37b42f99c946e6406aa180854f8af3b3d5397973..79a5340c5b1d1aa6ad5c2a7889b37edc54a3511d 100644 (file)
@@ -27,7 +27,7 @@
     </message>
     <message>
         <source>Powered by %1</source>
-        <translation type="unfinished"/>
+        <translation>%1 voimistama</translation>
     </message>
     <message>
         <source>Open-source software</source>
         <source>Please license %1</source>
         <translation>Lisensioi %1.</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Tämä demon kokeiluaika on päättynyt.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Täysversio mahdollistaa videoiden katselun ilman keskeytyksiä.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Ilman lisenssiä tämä sovellus vanhenee %1 päivässä.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Ostamalla täyden version tuet kovaa työtäni sovelluksen %1 parissa.</translation>
     <message>
         <source>K</source>
         <comment>K as in Kilo, i.e. thousands</comment>
-        <translation type="unfinished"/>
+        <translation>K</translation>
     </message>
     <message>
         <source>M</source>
         <comment>M stands for Millions</comment>
-        <translation type="unfinished"/>
+        <translation>M</translation>
     </message>
     <message>
         <source>B</source>
         <comment>B stands for Billions</comment>
-        <translation type="unfinished"/>
+        <translation>B</translation>
     </message>
     <message>
         <source>%1 views</source>
     </message>
     <message>
         <source>Switched to %1</source>
-        <translation type="unfinished"/>
+        <translation>Vaihdettiin tähän %1</translation>
     </message>
     <message>
         <source>Unsubscribed from %1</source>
         <source>Subscribed to %1</source>
         <translation>Tilattu %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation>Kelaa taaksepäin %1 sekuntia</translation>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation>Pikakelaa eteenpäin %1 sekuntia</translation>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation>kanava</translation>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Hanki täysversio</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation>Poista</translation>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation>Tarvitsetko kaukosäätimen %1 :lle? Kokeile %2!</translation>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation>Jatkan %1 :n parantamista tehdäkseni siitä niin hyvän kuin kykenen. Tue tätä työtä!</translation>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 7dd0a9900952e324e107b405cde85f689402f4e1..d41f8b310bb06653296241dc53d45d7e8d559cd2 100644 (file)
     </message>
     <message>
         <source>Powered by %1</source>
-        <translation type="unfinished"/>
+        <translation>%1 :n voimistama</translation>
     </message>
     <message>
         <source>Open-source software</source>
-        <translation type="unfinished"/>
+        <translation>Avoimen lähdekoodin ohjelma</translation>
     </message>
     <message>
         <source>Icon designed by %1.</source>
         <source>Please license %1</source>
         <translation>Lisensioi %1.</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Tämä demon kokeiluaika on päättynyt.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Täysversio mahdollistaa videoiden katselun ilman keskeytyksiä.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Ilman lisenssiä tämä sovellus vanhenee %1 päivässä.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Ostamalla täyden version tuet kovaa työtäni sovelluksen %1 parissa.</translation>
         <source>Subscribed to %1</source>
         <translation>Tilaa %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Hanki täysversio</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 46ff0f36f9ed138af6cfde22936bf3b8d3810819..49aa1c06e0c842f65ce4d701d802b39193941bf3 100644 (file)
         <source>Please license %1</source>
         <translation>Veuillez obtenir une licence pour %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Cette version de démonstration a expiré.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>La version complète vous permet de voir des vidéos sans interruption.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>En l&apos;absence de licence, l&apos;application expirera dans %1 jours.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>En achetant la version complète,vous supporterez aussi le grand travail que j&apos;ai investi dans la création de %1.</translation>
         <source>Subscribed to %1</source>
         <translation>S&apos;abonner à %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Obtenir la version complète</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 9bd8a11f055fed2187a3f1b51c170897f471fbe9..399a69a17e800369b96cd4c871dc4013cd241775 100644 (file)
         <source>Please license %1</source>
         <translation>Licenza %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Esta demo caducou.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>A versión completa permite ver os vídeos sen interrupcións.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Sen licenza o aplicativo expira en %1 días.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Ao comprar a versión completa, tamén vai apoiar o traballo arreo que puxen en crear %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Subscrito a %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Obter a versión completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index fffa9668d25453f0a9cd8775c71c0ba96d6a6fb7..0a252610241bcf03b07e4d3bd6262b986149a82f 100644 (file)
         <source>Please license %1</source>
         <translation>נא לרכוש את %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>גרסת הדגמה זו פגה.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>הגירסה המלאה מאפשרת לך לצפות בסרטון ללא הפרעות.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>ללא רישיון היישום יפוג בעוד %1 ימים.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>רכישת הגרסה המלאה מהווה תמיכה בעבודה הקשה שאני משקיע ביצירת %1.</translation>
         <source>Subscribed to %1</source>
         <translation>בוצע רישום ל%1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>קבלת הגרסה המלאה</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index ce41669bedfecb0a0edeee4ecf299adcafed6ae3..59f7225435c8edfbfc2211b1f4cf827847ae9925 100644 (file)
         <source>Please license %1</source>
         <translation>Molimo registrirajte %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Vaša probna verzija je istekla.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation type="unfinished"/>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Bez licence, ovaj će program isteći za %1 dana</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Kupnjom pune verzije podržati ćete i naporan rad uložen u izradu %1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Preuzmi punu verziju</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index fa4c83f7228a024db05209d3d6b8f42fe1b14734..a49a4a348c212b5f69f29cbfee90650168a20e73 100644 (file)
         <source>Please license %1</source>
         <translation>Szerezzen licencet a %1 programhoz</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>A demó változat lejárt.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>A teljes verzió lehetőséget nyújt a videók megszakítás nélküli lejátszására.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Licenc nélkül, a program %1 nap múlva lejár.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>A teljes verzió megvásárlásával a %1 programba fektetett munkámat is támogatja.</translation>
         <source>Subscribed to %1</source>
         <translation>Feliratkozva %1-ra</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Teljes verzió beszerzése</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 2672b63339c891d4c28dd869232d3fae1cc1b245..c988bb5b271868e35461e1583b1b0cf99237827b 100644 (file)
         <source>Please license %1</source>
         <translation>Silahkan lisensi %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Demo ini telah Habis.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Anda dapat melihat video tanpa gangguan dengan menggunakan versi penuh</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Tanpa lisensi, aplikasi akan kadaluarsa dalam %1 hari.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Dengan membeli versi penuh, Anda akan mendapatkan dukungan kerjakeras Saya letakkan kedalam keasi %1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Get the full version</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 3445da65ef220e314344065727be9e77a5b29ef0..8539f94d857623e2f665886d984b53a647f4c83e 100644 (file)
         <source>Please license %1</source>
         <translation>Compra %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Questa demo è scaduta.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>La versione completa ti permette di guardare i video senza interruzioni.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Senza una licenza, l&apos;applicazione scadrà in %1 giorni.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Acquistando la versione completa, supporterai anche il lavoro che ho fatto per creare %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Iscritto a %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Compra la versione completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index d20caeda920d074ebaec076d6716f88d532a8e9a..32e3234dc46e7d7c2c923ab89d9eeabe7af7ad9d 100644 (file)
         <source>Please license %1</source>
         <translation>%1のライセンスを取得してください</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>試用版の期限が切れました。</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>製品版では、快適に動画を視聴することができます。</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>現在は、ライセンスがないため、このアプリケーションは%1日に期限切れとなります。</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>あなたが製品版を購入することによって、%1の開発を支援することができます。</translation>
         <source>Subscribed to %1</source>
         <translation>%1を購読しました</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>製品版を入手する</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation>削除</translation>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 503df0efffa4a87c93ee1a748639ef1ffb3d4b73..1bfb777125c40ad1e413f5387b6a1895145f7334 100644 (file)
         <source>Please license %1</source>
         <translation>%1을(를) 구입하세요</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>데모버전이 만료되었습니다!</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>구입 버전은 방해받지 않고 비디오를 감상 할수 있습니다.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>라이센스를 구입하지 않으면, %1일후 만료 됩니다.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>구입하면, 개발자가 %1를 만드는데 드는 소중한 노력을 지원 합니다.</translation>
         <source>Subscribed to %1</source>
         <translation>%1 구독됨</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>풀 버전 구입</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index be95ecc6b2428ff8d3c291682282da0d9a521998..7a31971f338186cae6c509dccf3cb02dc7e1acc7 100644 (file)
         <source>Please license %1</source>
         <translation>%1 лицензиялап алыңыз</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Демострациялоо мөөнөтү бүттү.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation type="unfinished"/>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Лицензиясыз болгондуктан, тиркеменин мөөнөтү %1 күндөн кийин бүтөт.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Толук версиясын алсаңыз, %1 үчүн оор жумуш кылып жатканымды да колдоп бере аласыз.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Толук версиясын сатып алуу</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 9d6756bf00fd659b68e52cc1e23c5c4efd119d54..02f626cb54dee444840f57ba80071b52c2e51482 100644 (file)
         <source>Please license %1</source>
         <translation>Sila lesenkan %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Demo ini telah tamat tempoh.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Versi penuh membolehkan anda tonton video tanpa gangguan.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Tanpa lesen, aplikasi akan luput dalam tempoh %1 hari.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Dengan pembelian versi penuh, anda akan menyokong hasil kerja yang saya lakukan ketika menghasilkan %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Langgan kepada %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Dapatkan versi penuh</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index c0ebc3072f61d93212b879f05ec9097b1f70c1c7..7081fe58d3554112f12940d8232b50fde3e3feaa 100644 (file)
         <source>Please license %1</source>
         <translation>Vennligst lisensier %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Demoen er utløpt.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Den fulle versjonen tillater deg å se videoer uten avbrudd.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Uten lisens, vil programmet utløpe om %1 dager.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Ved å kjøpe fullversjonen, støtter du det harde arbeidet som ble lagt inn i å skape %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Abonnert på %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Kjøp fullversjon</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 3f2cbeb0f646e5450127f26e7d11381cf7fee2a0..4ccee44cfaed776ba1ab7e111faf060f77130619 100644 (file)
         <source>Please license %1</source>
         <translation>Alstublieft, Licenceer  %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Deze demo is verlopen.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>De volledige versie stelt u in staat om video&apos;s te bekijken zonder onderbrekingen.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Zonder een licentie zal deze applicatie vervallen in %1 dagen.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Door dit produkt te kopen beloon je mij voor het harde werk dat ik geïnvesteerd heb in het maken van %1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Verkrijg de volledige versie</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 40af51a304d109f05c51aee2abc1a4101a313fa1..ac1f51d3a0a115b69e70805736e4ee7b1a3ed579 100644 (file)
         <source>Please license %1</source>
         <translation>Ver venleg og lisensier %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Demoen har laupt ut.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation type="unfinished"/>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Utan ein lisens vil programmet gå ut på dato om %1 dagar.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Ved å kjøpa den fullstendige utgåva, støttar du arbeidet lagt inn i å skapa %1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Kjøpfullversjonen</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 760f2d1454f1e20cedcfb1508c92c861d76ce230..68434c6da15b61394520f8c2ea8e223db97526a5 100644 (file)
         <source>Please license %1</source>
         <translation>Proszę kupić licencję %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Okres wersji demo został zakończony.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Pełna wersja pozwoli Ci oglądać filmy bez przerw.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Bez licencji ta aplikacja wygaśnie za %1 dni.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Przez zakup pełnej wersji wesprzesz również ciężką pracę, którą włożyłem w stworzenie %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Rozpoczęto subskrypcję %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Uzyskaj pełną wersję</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 826dfb7ecfc8fbfb1b04e71a136e07a8bd568162..94a243e997e814b4b9e05e9dfccb589fe9315f1a 100644 (file)
         <source>Please license %1</source>
         <translation>Proszę kup licencję %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>To demo wygasło.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Pełna wersja pozwala na oglądanie filmów bez przeszkód.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Bez licencji, program wygaśnie za %1 dni.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Kupując pełną wersję, wspieraż także ciężką pracę, jaką włożyłem w stworzenie %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Zaczęto subskrypcję %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Pobierz pełną wersję</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index eb05ba19dc962c3222fd9154631179b267e7956b..a4312760897c8faf7f3a7a3bb2a38f123eeb7404 100644 (file)
         <source>Please license %1</source>
         <translation>Por favor licencie o %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Esta demo expirou.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>A versão compreta permite-lhe ver vídeos sem interrupções.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Sem uma licença, esta apliacação expirará em %1 dias.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Ao comprar a versão completa, estará a ajudar no desenvolvimento do %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Subscrito %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Obter a versão completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 2330982e25048296676e645e8658a2a4f65cbd18..fe5d0a09b20746c84e44f00a41e8b4bbda55ee58 100644 (file)
         <source>Please license %1</source>
         <translation>Por favor, licencie %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Esta demonstração expirou.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>A versão completa permite que você assista vídeos sem interrupções.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Sem licença, a aplicação irá expirar em %1 dias.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Ao comprar a versão completa, você também apoiará o trabalho duro que eu dediquei ao criar %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Inscrito em %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Obter a versão completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 6f0737e500436cc422a58725b4156fef67ddb9ca..548010b1846d64486e44bc4efae93d99d87bee3a 100644 (file)
         <source>Please license %1</source>
         <translation>Por favor licencie %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Esta demonstração expirou.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>A versão completa permite-lhe ver vídeos sem interrupções. </translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Sem uma licença, a aplicação irá expirar em %1 dias.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Ao comprar a versão completa, também irá apoiar o trabalho árduo que pus  %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Subscrito a %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Obter a versão completa</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 6f62bb5e0cc1a55de45a020e6ed3f8663cf88723..6c5bdace8562840dbf80db72079a5fc6eb9c0c45 100644 (file)
         <source>Please license %1</source>
         <translation>Licențiați %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Această versiune demo a expirat.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Versiunea nerestricționată vă permite să urmăriți clipurile video fără întreruperi.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Fără licență, aplicația va expira în %1 zile.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Cumpărând versiunea integrală îmi sprijini efortul depus pentru crearea %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Abonează-te la %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Obține versiunea integrală</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 1de2897b484c1e256206cefc8310e9c38c2e0807..1803b8839a791753fe5f5e369b334bd9b038a778 100644 (file)
         <translation>  
 Лицензия %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Демонстрационный период истек.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Полная версия позволит смотреть видео без пауз.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Срок работы, этого приложения, без лицензии истекает через %1 дн.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>При покупке полной версии, вы также оказываете поддержку тяжелой работы в создании %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Подписаны на %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Купить полную версию</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 61d4fd2f25eef0e62552c36c49b56d159cae5e83..5a823044a892113dee4a434f77c21439f98f4e23 100644 (file)
         <source>Please license %1</source>
         <translation>Prosím licencujte %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Demoverzia expirovala.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Plná verzia vám umožňuje pozerať videá bez prerušení.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Aplikácia bez licencie expiruje za %1 dní.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Zakúpením plnej verzie tiež podporíš vynaložené úsilie pri tvorbe %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Odoberáte kanál %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Získať plnú verziu</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 3f5ea5989a397f8e9653346a6a5885028b689e0b..b9e62211e8226727ac7d3786780acc57e9490c43 100644 (file)
         <source>Please license %1</source>
         <translation>Kupite prosim licenco %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Različica demo je potekla.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Polna verzija vam omogoča ogled posnetkov brez prekinitev.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Brez licence bo uporabnost aplikacije potekla čez %1 dni.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Z nakupom polne različice, boste podprli veliko truda, ki sem ga vložil v %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Prijavljeni ste na %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Pridobi celotno različico</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index bebe5187707606142db4c58c8b2140f57fd39815..1750165e67d9561134433e0cd46656d8c87d750e 100644 (file)
         <source>Please license %1</source>
         <translation>Ju lutem licenconi %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Versioni demostrues ka perfunduar</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation type="unfinished"/>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Pa licenc programmit do ti mbaron afati ne %1 ditë</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Duke bler versionin e plot , ju gjithashtu do te perkrahni punen e mundimshme qe kam ber per krijimin %1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Merrni versionin e plote</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 15e77deccafdb17a51fa2eaeb4c8c10df67e03f6..9505cebacd2a319e056d71cb8f53862c64cee599 100644 (file)
         <source>Please license %1</source>
         <translation>Молимо да купите %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Показна верзија је истекла.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Пуна верзија Вам дозвољава да гледате видео снимке без прекидања.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Без лиценце програм ће истећи за %1 дан(а).</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Куповином пуне верзије ћете уједно и подржати мој рад на програму %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Претплаћен на %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Преузмите пуну верзију</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 88d809e90b0e2ea6ebb41fc1e3cdf28a07a1c75e..69975d131bbfe67553d9e48080da6c294297ed27 100644 (file)
         <source>Please license %1</source>
         <translation>Vänligen licensiera %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Denna demo har utgått.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Den fulla versionen tillåter dig att kolla på videoklipp utan avbrott.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Utan en licens kommer programmet att upphöra inom %1 dagar.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Genom att köpa fullversionen kommer du också att stödja mitt hårda arbete jag gör med %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Prenumererad på %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Skaffa den kompletta versionen</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 0992c983d5234b63913f4217c917c26d08598266..00b0ddd3d2372e6ace0f2c72eac6389c6420d65e 100644 (file)
         <source>Please license %1</source>
         <translation>โปรดซื้อใบอนุญาต %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>ชุดทดลองใช้หมดอายุแล้ว</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>เวอร์ชั่นเต็มจะอนุญาตให้คุณรับชมวิดีโอโดยไม่ถูกขัดจังหวะ</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>ปราศจากใบอนุญาต แอปพลิเคชั่นจะหมดอายุใน %1 วัน</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>ด้วยการซื้อเวอร์ชั่นเต็ม คุณยังได้สนับสนุนการทำงานหนักที่ฉันใช้ในการสร้าง %1</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>ซื้อเวอร์ชั่นเต็ม</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 73a4a73aa6ee3f7a030895d613ef76e893006580..a96c9adec9f9adeb41adfb73b95f70c7cc0cc4b5 100644 (file)
         <source>Please license %1</source>
         <translation>Lütfen lisans %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Deneme süresi bitti.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Tam sürüm, videoları duraklamadan izlemenizi sağlar.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Lisans olmazsa, uygulama süresi %1 gün içinde dolacaktır.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Tam sürümü alarak, aynı zamanda %1 yaratmamdaki üstün çabamı destekleyebilirsiniz.</translation>
         <source>Subscribed to %1</source>
         <translation>%1 Abone Ol</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation>%1 saniye geri sar</translation>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation>%1 saniye hızlı ileri sar</translation>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation>kanal</translation>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Tam sürüme geç</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation>Kaldır</translation>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation>%1 için uzaktan kumanda mı lazım? %2&apos;yi dene!</translation>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation>%1 iyileştirmek için sürekli devam ediyorum. Bu çalışmayı destekleyin!</translation>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index fccc3709cf349b21aaaa1b5a9a4afd6539473003..36b9cd4af35c9afb2bdbcf46e54f270019ff5cda 100644 (file)
         <translation>  
 Ліцензуйте %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Пробний період вичерпано.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Повна версія дозволяє дивитися видиво без перерв.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Проґраму буде заблоковано через %1 днів, якщо Ви не придбаєте ліцензію.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>При купівлі повної версії, Ви, також, надаєте підтримку важкій роботі, яка була задіяна під час створення %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Підписані на %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Отримати повну версію</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 9c41c384a50325b137b2e37a537674577e84775a..0eba359050e2a3c3f2644fcab44d0d64194ca464 100644 (file)
         <translation>  
 Ліцензуйте %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Пробний період вичерпано.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>Повна версія дозволяє дивитися видиво без перерв.</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Проґраму буде заблоковано через %1 днів, якщо Ви не придбаєте ліцензію.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>При купівлі повної версії, Ви, також, надаєте підтримку важкій роботі, яка була задіяна під час створення %1.</translation>
         <source>Subscribed to %1</source>
         <translation>Підписані на %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Отримати повну версію</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index dce52fb1eeba256d3196672a9cb7c83c1d99c133..c29e0390e9fa48f8983b0cd925ad288bcb9e1218 100644 (file)
         <source>Please license %1</source>
         <translation>Xin giấy phép %1</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>Phần thử nghiệm này đã hết hạn.</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation type="unfinished"/>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>Nếu không có giấy phép, các ứng dụng sẽ hết hạn trong %1 ngày.</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>Bằng cách mua phiên bản đầy đủ, bạn cũng sẽ hỗ trợ vào quá trình phát triển của phần mềm %1.</translation>
         <source>Subscribed to %1</source>
         <translation type="unfinished"/>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>Nhận phiên bản đầy đủ</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation type="unfinished"/>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 27a7517eaf1043cb44d15396fbe5c3c961fa08bd..ff7cabb2eba634dd1cac23a71bf9aab28875a461 100644 (file)
     </message>
     <message>
         <source>Powered by %1</source>
-        <translation type="unfinished"/>
+        <translation>由 %1 强力驱动</translation>
     </message>
     <message>
         <source>Open-source software</source>
-        <translation type="unfinished"/>
+        <translation>开源软件</translation>
     </message>
     <message>
         <source>Icon designed by %1.</source>
         <source>Please license %1</source>
         <translation>请购买 %1 授权</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>该演示版已经过期。</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>完整版本允许你不被中断观看视频</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>若无授权,该软件将会在 %1 天后过期。</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>通过购买完整版本,您也将支持我在创建 %1 时的辛苦工作。</translation>
     </message>
     <message numerus="yes">
         <source>You have %n new video(s)</source>
-        <translation type="unfinished"><numerusform></numerusform></translation>
+        <translation><numerusform>你有 %n 个新视频</numerusform></translation>
     </message>
 </context>
 <context>
     </message>
     <message numerus="yes">
         <source>%n hour(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform></translation>
+        <translation><numerusform>%n 小时前</numerusform></translation>
     </message>
     <message numerus="yes">
         <source>%n day(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform></translation>
+        <translation><numerusform>%n 天前</numerusform></translation>
     </message>
     <message numerus="yes">
         <source>%n month(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform></translation>
+        <translation><numerusform>%n 月前</numerusform></translation>
     </message>
     <message>
         <source>K</source>
         <comment>K as in Kilo, i.e. thousands</comment>
-        <translation type="unfinished"/>
+        <translation>K</translation>
     </message>
     <message>
         <source>M</source>
         <comment>M stands for Millions</comment>
-        <translation type="unfinished"/>
+        <translation>M</translation>
     </message>
     <message>
         <source>B</source>
         <comment>B stands for Billions</comment>
-        <translation type="unfinished"/>
+        <translation>B</translation>
     </message>
     <message>
         <source>%1 views</source>
     </message>
     <message numerus="yes">
         <source>%n week(s) ago</source>
-        <translation type="unfinished"><numerusform></numerusform></translation>
+        <translation><numerusform>%n 周前</numerusform></translation>
     </message>
 </context>
 <context>
     </message>
     <message numerus="yes">
         <source>%n Download(s)</source>
-        <translation type="unfinished"><numerusform></numerusform></translation>
+        <translation><numerusform>%n 个下载</numerusform></translation>
     </message>
 </context>
 <context>
     </message>
     <message>
         <source>Toggle &amp;Menu Bar</source>
-        <translation type="unfinished"/>
+        <translation>切换菜单栏(&amp;M)</translation>
     </message>
     <message>
         <source>Menu</source>
-        <translation type="unfinished"/>
+        <translation>菜单</translation>
     </message>
     <message>
         <source>&amp;Love %1? Rate it!</source>
     </message>
     <message>
         <source>You can still access the menu bar by pressing the ALT key</source>
-        <translation type="unfinished"/>
+        <translation>按住 ALT 键以访问菜单栏</translation>
     </message>
 </context>
 <context>
     </message>
     <message>
         <source>Switched to %1</source>
-        <translation type="unfinished"/>
+        <translation>切换到 %1</translation>
     </message>
     <message>
         <source>Unsubscribed from %1</source>
         <source>Subscribed to %1</source>
         <translation>订阅 %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation>回退 %1 秒</translation>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation>快进 %1 秒</translation>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation>频道</translation>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
     <name>PickMessage</name>
     <message>
         <source>Pick a video</source>
-        <translation type="unfinished"/>
+        <translation>选择一个视频</translation>
     </message>
 </context>
 <context>
         <source>Get the full version</source>
         <translation>获取完整版</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation>移除</translation>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation>想要远程控制 %1?试试 %2!</translation>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation>我一直在尽力改进 %1。请支持这个作品!</translation>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
     </message>
     <message>
         <source>&amp;Forward</source>
-        <translation type="unfinished"/>
+        <translation>前进(&amp;F)</translation>
     </message>
     <message>
         <source>Forward to %1</source>
index 854e248d824cbac02fc2df2cb2067e1388392131..94154ae62122f73dbe98b1b3d821d8bb3cc08f0b 100644 (file)
         <source>Please license %1</source>
         <translation>請取得 %1 授權</translation>
     </message>
-    <message>
-        <source>This demo has expired.</source>
-        <translation>這個展示版已過期。</translation>
-    </message>
     <message>
         <source>The full version allows you to watch videos without interruptions.</source>
         <translation>完整的版本允許您不被中斷地觀看影片。</translation>
     </message>
-    <message>
-        <source>Without a license, the application will expire in %1 days.</source>
-        <translation>沒有取得授權,本程式將於 %1 天過期。</translation>
-    </message>
     <message>
         <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
         <translation>透過購買完整版,您還可以支持我投入更多心力於打造 %1。</translation>
         <source>Subscribed to %1</source>
         <translation>已訂閱 %1</translation>
     </message>
+    <message>
+        <source>Rewind %1 seconds</source>
+        <translation>倒帶 %1 秒</translation>
+    </message>
+    <message>
+        <source>Fast forward %1 seconds</source>
+        <translation>快轉 %1 秒</translation>
+    </message>
+    <message>
+        <source>channel</source>
+        <translation>頻道</translation>
+    </message>
 </context>
 <context>
     <name>MessageWidget</name>
         <source>Get the full version</source>
         <translation>取得完整版</translation>
     </message>
+    <message>
+        <source>Remove</source>
+        <translation>移除</translation>
+    </message>
+    <message>
+        <source>Need a remote control for %1? Try %2!</source>
+        <translation>需要遙控 %1?試試看 %2!</translation>
+    </message>
+    <message>
+        <source>I keep improving %1 to make it the best I can. Support this work!</source>
+        <translation>我會不斷改善 %1,讓它變得更好。請支持我的努力!</translation>
+    </message>
 </context>
 <context>
     <name>SidebarHeader</name>
index 652392ff32be7287469a29f3a7a1032ccb9ab5f9..187db44e2d8e2967be06fdc3afb233e8eba63cdf 100644 (file)
@@ -1,7 +1,7 @@
-CONFIG += c++14 exceptions_off rtti_off optimize_full
+CONFIG += c++17 exceptions_off rtti_off optimize_full object_parallel_to_source
 
 TEMPLATE = app
-VERSION = 3.4.2
+VERSION = 3.5
 DEFINES += APP_VERSION="$$VERSION"
 
 APP_NAME = Minitube
@@ -10,6 +10,9 @@ DEFINES += APP_NAME="$$APP_NAME"
 APP_UNIX_NAME = minitube
 DEFINES += APP_UNIX_NAME="$$APP_UNIX_NAME"
 
+message(Building $${APP_NAME} $${VERSION})
+message(Qt $$[QT_VERSION] in $$[QT_INSTALL_PREFIX])
+
 DEFINES += APP_SNAPSHOT
 
 CONFIG -= debug_and_release
@@ -38,14 +41,16 @@ DEFINES += MEDIA_MPV
 include(lib/media/media.pri)
 
 include(src/qtsingleapplication/qtsingleapplication.pri)
+include(src/invidious/invidious.pri)
 
 HEADERS += src/video.h \
     src/messagebar.h \
     src/spacer.h \
     src/constants.h \
     src/playlistitemdelegate.h \
+    src/updateutils.h \
+    src/videoapi.h \
     src/videomimedata.h \
-    src/updatechecker.h \
     src/searchparams.h \
     src/minisplitter.h \
     src/loadingwidget.h \
@@ -76,6 +81,7 @@ HEADERS += src/video.h \
     src/view.h \
     src/playlistmodel.h \
     src/videosource.h \
+    src/waitingspinnerwidget.h \
     src/ytsearch.h \
     src/ytstandardfeed.h \
     src/standardfeedsview.h \
@@ -118,9 +124,9 @@ HEADERS += src/video.h \
 SOURCES += src/main.cpp \
     src/messagebar.cpp \
     src/spacer.cpp \
+    src/updateutils.cpp \
     src/video.cpp \
     src/videomimedata.cpp \
-    src/updatechecker.cpp \
     src/searchparams.cpp \
     src/minisplitter.cpp \
     src/loadingwidget.cpp \
@@ -151,6 +157,7 @@ SOURCES += src/main.cpp \
     src/playlistitemdelegate.cpp \
     src/playlistmodel.cpp \
     src/videosource.cpp \
+    src/waitingspinnerwidget.cpp \
     src/ytsearch.cpp \
     src/ytstandardfeed.cpp \
     src/standardfeedsview.cpp \
@@ -266,6 +273,11 @@ unix:!mac {
 
 mac|win32|contains(DEFINES, APP_UBUNTU):include(local/local.pri)
 
+!contains(DEFINES, APP_MAC_STORE) {
+    # DEFINES += UPDATER_NO_SPARKLE
+    include(lib/updater/updater.pri)
+}
+
 message(CONFIG: $$CONFIG)
 message(DEFINES: $$DEFINES)
 message(QMAKE_CXXFLAGS: $$QMAKE_CXXFLAGS)
index 8820f2df0a26dc262ccf43f90d67d364567dd467..48b7dff03b466ae8b9daf1e67c791d33b44d68d0 100644 (file)
@@ -36,6 +36,11 @@ $END_LICENSE */
 #include "iconutils.h"
 #include "mainwindow.h"
 
+#ifdef UPDATER
+#include "updater.h"
+#include "waitingspinnerwidget.h"
+#endif
+
 AboutView::AboutView(QWidget *parent) : View(parent) {
     const int padding = 30;
     const char *buildYear = __DATE__ + 7;
@@ -67,6 +72,7 @@ AboutView::AboutView(QWidget *parent) : View(parent) {
     QBoxLayout *layout = new QVBoxLayout();
     layout->setAlignment(Qt::AlignCenter);
     layout->setSpacing(padding);
+    layout->setMargin(padding / 2);
     aboutlayout->addLayout(layout);
 
     QColor lightTextColor = palette().text().color();
@@ -146,8 +152,42 @@ AboutView::AboutView(QWidget *parent) : View(parent) {
     infoLabel->setWordWrap(true);
     layout->addWidget(infoLabel);
 
+#ifdef UPDATER
+    int capHeight = fontMetrics().capHeight();
+
+    QBoxLayout *updateLayout = new QHBoxLayout();
+    updateLayout->setMargin(0);
+    updateLayout->setSpacing(capHeight);
+    updateLayout->setAlignment(Qt::AlignLeft);
+
+    auto spinner = new WaitingSpinnerWidget(this, false, false);
+    spinner->setColor(palette().foreground().color());
+    spinner->setLineLength(capHeight / 2);
+    spinner->setNumberOfLines(spinner->lineLength() * 2);
+    spinner->setInnerRadius(spinner->lineLength());
+    auto spinnerStartStop = [spinner](auto status) {
+        if (status == Updater::Status::DownloadingUpdate)
+            spinner->start();
+        else
+            spinner->stop();
+    };
+    connect(&Updater::instance(), &Updater::statusChanged, this, spinnerStartStop);
+    updateLayout->addWidget(spinner);
+    spinnerStartStop(Updater::instance().getStatus());
+
+    updateLayout->addWidget(Updater::instance().getLabel());
+
+    layout->addLayout(updateLayout);
+#endif
+
     QLayout *buttonLayout = new QHBoxLayout();
+    buttonLayout->setMargin(0);
     buttonLayout->setAlignment(Qt::AlignLeft);
+
+#ifdef UPDATER
+    buttonLayout->addWidget(Updater::instance().getButton());
+#endif
+
     closeButton = new QPushButton(tr("&Close"), this);
     closeButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
     closeButton->setDefault(true);
@@ -163,4 +203,7 @@ AboutView::AboutView(QWidget *parent) : View(parent) {
 
 void AboutView::appear() {
     closeButton->setFocus();
+#ifdef UPDATER
+    Updater::instance().checkWithoutUI();
+#endif
 }
index 8b9996a5ac5d01bc25d8d4ce2b249ccdfc5b985b..3c67e28806ab73f3cedb081373d10ae018848326 100644 (file)
@@ -42,7 +42,8 @@ void AppsWidget::paintEvent(QPaintEvent *e) {
     style()->drawPrimitive(QStyle::PE_Widget, &o, &p, this);
 }
 
-AppWidget::AppWidget(const QString &name, const QString &code, QWidget *parent) : QWidget(parent), icon(0), name(name), downloadButton(0) {
+AppWidget::AppWidget(const QString &name, const QString &code, QWidget *parent)
+    : QWidget(parent), name(name), downloadButton(nullptr) {
     const QString unixName = code.left(code.lastIndexOf('.'));
     const QString baseUrl = QLatin1String("https://") + Constants::ORG_DOMAIN;
     const QString filesUrl = baseUrl + QLatin1String("/files/");
@@ -56,7 +57,11 @@ AppWidget::AppWidget(const QString &name, const QString &code, QWidget *parent)
     icon = new QLabel();
     icon->setMinimumHeight(128);
     layout->addWidget(icon);
-    const QString iconUrl = filesUrl + QLatin1String("products/") + unixName + QLatin1String(".png");
+    QString pixelRatioString;
+    if (devicePixelRatioF() > 1.0)
+        pixelRatioString = '@' + QString::number(devicePixelRatio()) + 'x';
+    const QString iconUrl = filesUrl + QLatin1String("products/") + unixName + pixelRatioString +
+                            QLatin1String(".png");
     QObject *reply = Http::instance().get(iconUrl);
     connect(reply, SIGNAL(data(QByteArray)), SLOT(iconDownloaded(QByteArray)));
 
index a4cd7d2e184b53ba3d72635076089fd6ee7d6c17..5848fa9e935d47bf908db7e039871fa7c1d452fc 100644 (file)
@@ -30,9 +30,12 @@ $END_LICENSE */
 #include "http.h"
 #include "httputils.h"
 
+#include "videoapi.h"
+#include "ivchannelsource.h"
+
 ChannelAggregator::ChannelAggregator(QObject *parent)
     : QObject(parent), unwatchedCount(-1), running(false), stopped(false), currentChannel(0) {
-    checkInterval = 1800;
+    checkInterval = 3600;
 
     timer = new QTimer(this);
     timer->setInterval(60000 * 5);
@@ -47,7 +50,7 @@ ChannelAggregator *ChannelAggregator::instance() {
 void ChannelAggregator::start() {
     stopped = false;
     updateUnwatchedCount();
-    QTimer::singleShot(0, this, SLOT(run()));
+    QTimer::singleShot(10000, this, SLOT(run()));
     if (!timer->isActive()) timer->start();
 }
 
@@ -112,6 +115,8 @@ void ChannelAggregator::parseWebPage(const QByteArray &bytes) {
         QString latestVideoId = currentChannel->latestVideoId();
         qDebug() << "Comparing" << videoId << latestVideoId;
         hasNewVideos = videoId != latestVideoId;
+    } else {
+        qDebug() << "Cannot capture latest video id";
     }
     if (hasNewVideos) {
         if (currentChannel) {
@@ -137,9 +142,18 @@ void ChannelAggregator::reallyProcessChannel(YTChannel *channel) {
     params->setSortBy(SearchParams::SortByNewest);
     params->setTransient(true);
     params->setPublishedAfter(channel->getChecked());
-    YTSearch *videoSource = new YTSearch(params);
-    connect(videoSource, SIGNAL(gotVideos(QVector<Video *>)), SLOT(videosLoaded(QVector<Video *>)));
-    videoSource->loadVideos(50, 1);
+
+    if (VideoAPI::impl() == VideoAPI::YT3) {
+        YTSearch *videoSource = new YTSearch(params);
+        connect(videoSource, SIGNAL(gotVideos(QVector<Video *>)),
+                SLOT(videosLoaded(QVector<Video *>)));
+        videoSource->loadVideos(50, 1);
+    } else if (VideoAPI::impl() == VideoAPI::IV) {
+        auto *videoSource = new IVChannelSource(params);
+        connect(videoSource, SIGNAL(gotVideos(QVector<Video *>)),
+                SLOT(videosLoaded(QVector<Video *>)));
+        videoSource->loadVideos(50, 1);
+    }
 
     channel->updateChecked();
 }
index 0ce9a2c313cf64137119be10248f14c2e3fadcd4..0e93c732261364556b3216cab85236b153dd26c4 100644 (file)
@@ -34,6 +34,9 @@ $END_LICENSE */
 #endif
 #include "channellistview.h"
 
+#include "videoapi.h"
+#include "ivchannelsource.h"
+
 namespace {
 const QString sortByKey = "subscriptionsSortBy";
 const QString showUpdatedKey = "subscriptionsShowUpdated";
@@ -168,9 +171,15 @@ void ChannelView::itemActivated(const QModelIndex &index) {
         params->setChannelId(channel->getChannelId());
         params->setSortBy(SearchParams::SortByNewest);
         params->setTransient(true);
-        YTSearch *videoSource = new YTSearch(params);
-        videoSource->setAsyncDetails(true);
-        emit activated(videoSource);
+        VideoSource *vs = nullptr;
+        if (VideoAPI::impl() == VideoAPI::YT3) {
+            YTSearch *videoSource = new YTSearch(params);
+            videoSource->setAsyncDetails(true);
+            vs = videoSource;
+        } else if (VideoAPI::impl() == VideoAPI::IV) {
+            vs = new IVChannelSource(params);
+        }
+        emit activated(vs);
         channel->updateWatched();
     } else if (itemType == ChannelModel::ItemAggregate) {
         AggregateVideoSource *videoSource = new AggregateVideoSource();
index 76bac4d74b15edef2afa862d0f1b824c8c9fb164..6393fe3e0018e74159ce55bf2afe05623359bfba 100644 (file)
@@ -3,7 +3,6 @@
 #include "constants.h"
 #include "http.h"
 #include "localcache.h"
-#include "throttledhttp.h"
 
 Http &HttpUtils::notCached() {
     static Http *h = [] {
@@ -33,7 +32,7 @@ Http &HttpUtils::yt() {
         http->addRequestHeader("User-Agent", stealthUserAgent());
 
         CachedHttp *cachedHttp = new CachedHttp(*http, "yt");
-        cachedHttp->setMaxSeconds(3600);
+        cachedHttp->setMaxSeconds(86400);
 
         return cachedHttp;
     }();
@@ -67,7 +66,7 @@ const QByteArray &HttpUtils::userAgent() {
 
 const QByteArray &HttpUtils::stealthUserAgent() {
     static const QByteArray ua =
-            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like "
-            "Gecko) Chrome/79.0.3945.79 Safari/537.36";
+            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like "
+            "Gecko) Chrome/84.0.4147.105 Safari/537.36";
     return ua;
 }
diff --git a/src/invidious/invidious.cpp b/src/invidious/invidious.cpp
new file mode 100644 (file)
index 0000000..38faf9a
--- /dev/null
@@ -0,0 +1,95 @@
+#include "invidious.h"
+
+#include "cachedhttp.h"
+#include "http.h"
+#include "httputils.h"
+#include "throttledhttp.h"
+
+Invidious &Invidious::instance() {
+    static Invidious i;
+    return i;
+}
+
+Http &Invidious::http() {
+    static Http *h = [] {
+        Http *http = new Http;
+        http->addRequestHeader("User-Agent", HttpUtils::stealthUserAgent());
+        http->setMaxRetries(0);
+        return http;
+    }();
+    return *h;
+}
+
+Http &Invidious::cachedHttp() {
+    static Http *h = [] {
+        ThrottledHttp *throttledHttp = new ThrottledHttp(http());
+        throttledHttp->setMilliseconds(300);
+
+        CachedHttp *cachedHttp = new CachedHttp(*throttledHttp, "iv");
+        cachedHttp->setMaxSeconds(86400);
+        cachedHttp->setIgnoreHostname(true);
+        return cachedHttp;
+    }();
+    return *h;
+}
+
+Invidious::Invidious(QObject *parent) : QObject(parent) {}
+
+void Invidious::initServers() {
+    servers.clear();
+    QUrl url("https://instances.invidio.us/instances.json?sort_by=type,health,users");
+    auto reply = HttpUtils::yt().get(url);
+    connect(reply, &HttpReply::finished, this, [this](auto &reply) {
+        if (reply.isSuccessful()) {
+            QSettings settings;
+            QStringList keywords = settings.value("recentKeywords").toStringList();
+            QString testKeyword = keywords.isEmpty() ? "test" : keywords.first();
+
+            bool haveEnoughServers = false;
+            QJsonDocument doc = QJsonDocument::fromJson(reply.body());
+            for (const auto &v : doc.array()) {
+                auto serverArray = v.toArray();
+                QString host = serverArray.first().toString();
+                QJsonObject serverObj = serverArray.at(1).toObject();
+                if (serverObj["type"] == "https") {
+                    QString url = "https://" + host;
+
+                    if (haveEnoughServers) break;
+                    QUrl testUrl(url + "/api/v1/search?q=" + testKeyword);
+                    auto reply = http().get(testUrl);
+                    connect(reply, &HttpReply::finished, this,
+                            [this, url, &haveEnoughServers](auto &reply) {
+                                if (!haveEnoughServers && reply.isSuccessful()) {
+                                    QJsonDocument doc = QJsonDocument::fromJson(reply.body());
+                                    if (!doc.array().isEmpty()) {
+                                        servers << url;
+                                        if (servers.size() > 4) {
+                                            haveEnoughServers = true;
+                                            std::shuffle(servers.begin(), servers.end(),
+                                                         *QRandomGenerator::global());
+                                            qDebug() << servers;
+                                            emit serversInitialized();
+                                        }
+                                    }
+                                }
+                            });
+                }
+            }
+        }
+    });
+}
+
+QString Invidious::baseUrl() {
+    QString host;
+    if (servers.isEmpty())
+        host = "https://invidious.snopyta.org";
+    else
+        host = servers.first();
+    QString url = host + QLatin1String("/api/v1/");
+    return url;
+}
+
+QUrl Invidious::method(const QString &name) {
+    QUrl url(baseUrl() + name);
+    return url;
+}
diff --git a/src/invidious/invidious.h b/src/invidious/invidious.h
new file mode 100644 (file)
index 0000000..65de4c0
--- /dev/null
@@ -0,0 +1,28 @@
+#ifndef INVIDIOUS_H
+#define INVIDIOUS_H
+
+#include <QtNetwork>
+
+class Http;
+
+class Invidious : public QObject {
+    Q_OBJECT
+
+public:
+    static Invidious &instance();
+    static Http &http();
+    static Http &cachedHttp();
+
+    explicit Invidious(QObject *parent = nullptr);
+    void initServers();
+    QString baseUrl();
+    QUrl method(const QString &name);
+
+signals:
+    void serversInitialized();
+
+private:
+    QStringList servers;
+};
+
+#endif // INVIDIOUS_H
diff --git a/src/invidious/invidious.pri b/src/invidious/invidious.pri
new file mode 100644 (file)
index 0000000..b6256bd
--- /dev/null
@@ -0,0 +1,19 @@
+INCLUDEPATH += $$PWD
+DEPENDPATH += $$PWD
+
+HEADERS += $$PWD/invidious.h \
+    $$PWD/ivchannel.h \
+    $$PWD/ivchannelsource.h \
+    $$PWD/ivlistparser.h \
+    $$PWD/ivsearch.h \
+    $$PWD/ivsinglevideosource.h \
+    $$PWD/ivvideolist.h
+
+SOURCES += $$PWD/invidious.cpp \
+    $$PWD/ivchannel.cpp \
+    $$PWD/ivchannelsource.cpp \
+    $$PWD/ivlistparser.cpp \
+    $$PWD/ivsearch.cpp \
+    $$PWD/ivsinglevideosource.cpp \
+    $$PWD/ivvideolist.cpp
+
diff --git a/src/invidious/ivchannel.cpp b/src/invidious/ivchannel.cpp
new file mode 100644 (file)
index 0000000..6b96e1f
--- /dev/null
@@ -0,0 +1,34 @@
+#include "ivchannel.h"
+
+#include "http.h"
+#include "httputils.h"
+#include "invidious.h"
+
+IVChannel::IVChannel(const QString &id, QObject *parent) : QObject(parent) {
+    QUrl url = Invidious::instance().method("channels/");
+    url.setPath(url.path() + id);
+
+    auto *reply = Invidious::cachedHttp().get(url);
+    connect(reply, &HttpReply::data, this, [this](auto data) {
+        QJsonDocument doc = QJsonDocument::fromJson(data);
+        const QJsonObject obj = doc.object();
+
+        displayName = obj["author"].toString();
+        description = obj["descriptionHtml"].toString();
+
+        const auto thumbnails = obj["authorThumbnails"].toArray();
+        for (const auto &thumbnail : thumbnails) {
+            if (thumbnail["width"].toInt() >= 300) {
+                thumbnailUrl = thumbnail["url"].toString();
+                break;
+            }
+        }
+        qDebug() << displayName << description << thumbnailUrl;
+
+        emit loaded();
+    });
+    connect(reply, &HttpReply::error, this, [this](auto message) {
+        Invidious::instance().initServers();
+        emit error(message);
+    });
+}
diff --git a/src/invidious/ivchannel.h b/src/invidious/ivchannel.h
new file mode 100644 (file)
index 0000000..e88768a
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef IVCHANNEL_H
+#define IVCHANNEL_H
+
+#include <QtCore>
+
+class IVChannel : public QObject {
+    Q_OBJECT
+
+public:
+    IVChannel(const QString &id, QObject *parent = nullptr);
+
+    QString getDisplayName() const { return displayName; }
+    QString getDescription() const { return description; }
+    QString getThumbnailUrl() const { return thumbnailUrl; }
+
+signals:
+    void loaded();
+    void error(QString message);
+
+private:
+    QString displayName;
+    QString description;
+    QString thumbnailUrl;
+};
+
+#endif // IVCHANNEL_H
diff --git a/src/invidious/ivchannelsource.cpp b/src/invidious/ivchannelsource.cpp
new file mode 100644 (file)
index 0000000..f486517
--- /dev/null
@@ -0,0 +1,90 @@
+#include "ivchannelsource.h"
+
+#include "http.h"
+#include "httputils.h"
+#include "invidious.h"
+#include "ivlistparser.h"
+#include "mainwindow.h"
+#include "searchparams.h"
+#include "video.h"
+
+namespace {
+int invidiousFixedMax = 20;
+}
+
+IVChannelSource::IVChannelSource(SearchParams *searchParams, QObject *parent)
+    : VideoSource(parent), searchParams(searchParams) {
+    searchParams->setParent(this);
+}
+
+void IVChannelSource::loadVideos(int max, int startIndex) {
+    aborted = false;
+
+    QUrl url = Invidious::instance().method("channels/videos/");
+    url.setPath(url.path() + searchParams->channelId());
+
+    QUrlQuery q(url);
+
+    int page = ((startIndex - 1) / invidiousFixedMax) + 1;
+    q.addQueryItem("page", QString::number(page));
+
+    switch (searchParams->sortBy()) {
+    case SearchParams::SortByNewest:
+        q.addQueryItem("sort_by", "newest");
+        break;
+    case SearchParams::SortByViewCount:
+        q.addQueryItem("sort_by", "popular");
+        break;
+    }
+
+    url.setQuery(q);
+
+    auto *reply = Invidious::cachedHttp().get(url);
+    connect(reply, &HttpReply::data, this, [this](auto data) {
+        QJsonDocument doc = QJsonDocument::fromJson(data);
+        const QJsonArray items = doc.array();
+        IVListParser parser(items);
+        const QVector<Video *> &videos = parser.getVideos();
+
+        if (items.size() > invidiousFixedMax) invidiousFixedMax = items.size();
+
+        if (name.isEmpty() && !searchParams->channelId().isEmpty()) {
+            if (!videos.isEmpty()) {
+                name = videos.at(0)->getChannelTitle();
+                if (!searchParams->keywords().isEmpty()) {
+                    name += QLatin1String(": ") + searchParams->keywords();
+                }
+            }
+            emit nameChanged(name);
+        }
+
+        emit gotVideos(videos);
+        emit finished(videos.size());
+    });
+    connect(reply, &HttpReply::error, this, [this](auto message) {
+        Invidious::instance().initServers();
+        emit error(message);
+    });
+}
+
+void IVChannelSource::abort() {
+    aborted = true;
+}
+
+QString IVChannelSource::getName() {
+    return name;
+}
+
+const QList<QAction *> &IVChannelSource::getActions() {
+    static const QList<QAction *> channelActions = {
+            MainWindow::instance()->getAction("subscribeChannel")};
+    if (searchParams->channelId().isEmpty()) {
+        static const QList<QAction *> noActions;
+        return noActions;
+    }
+    return channelActions;
+}
+
+int IVChannelSource::maxResults() {
+    return invidiousFixedMax;
+}
diff --git a/src/invidious/ivchannelsource.h b/src/invidious/ivchannelsource.h
new file mode 100644 (file)
index 0000000..a1deaf7
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef IVCHANNELSOURCE_H
+#define IVCHANNELSOURCE_H
+
+#include "videosource.h"
+#include <QtNetwork>
+
+class SearchParams;
+
+class IVChannelSource : public VideoSource {
+    Q_OBJECT
+
+public:
+    IVChannelSource(SearchParams *searchParams, QObject *parent = nullptr);
+
+    void loadVideos(int max, int startIndex);
+    void abort();
+    QString getName();
+    const QList<QAction *> &getActions();
+    int maxResults();
+
+    SearchParams *getSearchParams() const { return searchParams; }
+
+private:
+    SearchParams *searchParams;
+    bool aborted;
+    QString name;
+};
+
+#endif // IVCHANNELSOURCE_H
diff --git a/src/invidious/ivlistparser.cpp b/src/invidious/ivlistparser.cpp
new file mode 100644 (file)
index 0000000..7c03e6f
--- /dev/null
@@ -0,0 +1,68 @@
+#include "ivlistparser.h"
+
+#include "video.h"
+
+namespace {
+
+QString decodeEntities(const QString &s) {
+    return QTextDocumentFragment::fromHtml(s).toPlainText();
+}
+
+} // namespace
+
+IVListParser::IVListParser(const QJsonArray &items) {
+    videos.reserve(items.size());
+    for (const QJsonValue &v : items) {
+        QJsonObject item = v.toObject();
+        parseItem(item);
+    }
+}
+
+void IVListParser::parseItem(const QJsonObject &item) {
+    Video *video = new Video();
+
+    QJsonValue id = item[QLatin1String("videoId")];
+    video->setId(id.toString());
+
+    bool isLiveBroadcastContent = item[QLatin1String("liveNow")].toBool();
+    if (isLiveBroadcastContent) {
+        delete video;
+        return;
+    }
+
+    int publishedAt = item[QLatin1String("published")].toInt();
+    QDateTime publishedDateTime = QDateTime::fromSecsSinceEpoch(publishedAt);
+    video->setPublished(publishedDateTime);
+
+    video->setChannelId(item[QLatin1String("authorId")].toString());
+
+    QString title = item[QLatin1String("title")].toString();
+    static const QChar ampersand('&');
+    if (title.contains(ampersand)) title = decodeEntities(title);
+    video->setTitle(title);
+    video->setDescription(item[QLatin1String("descriptionHtml")].toString());
+
+    const auto thumbnails = item[QLatin1String("videoThumbnails")].toArray();
+    for (const auto &thumbnail : thumbnails) {
+        auto q = thumbnail["quality"];
+        if (q == QLatin1String("medium")) {
+            video->setThumbnailUrl(thumbnail["url"].toString());
+        } else if (q == QLatin1String("high")) {
+            video->setMediumThumbnailUrl(thumbnail["url"].toString());
+        } else if (q == QLatin1String("sddefault")) {
+            video->setLargeThumbnailUrl(thumbnail["url"].toString());
+        }
+    }
+
+    video->setChannelTitle(item[QLatin1String("author")].toString());
+
+    // These are only for "videos" requests
+
+    int duration = item[QLatin1String("lengthSeconds")].toInt();
+    video->setDuration(duration);
+
+    int viewCount = item[QLatin1String("viewCount")].toInt();
+    video->setViewCount(viewCount);
+
+    videos.append(video);
+}
diff --git a/src/invidious/ivlistparser.h b/src/invidious/ivlistparser.h
new file mode 100644 (file)
index 0000000..c171fea
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef IVLISTPARSER_H
+#define IVLISTPARSER_H
+
+#include <QtCore>
+
+class Video;
+
+class IVListParser {
+public:
+    IVListParser(const QJsonArray &items);
+    const QVector<Video *> &getVideos() { return videos; }
+
+private:
+    void parseItem(const QJsonObject &item);
+
+    QVector<Video *> videos;
+};
+
+#endif // IVLISTPARSER_H
diff --git a/src/invidious/ivsearch.cpp b/src/invidious/ivsearch.cpp
new file mode 100644 (file)
index 0000000..96ae9f9
--- /dev/null
@@ -0,0 +1,140 @@
+#include "ivsearch.h"
+
+#include "http.h"
+#include "httputils.h"
+#include "invidious.h"
+#include "ivlistparser.h"
+#include "mainwindow.h"
+#include "searchparams.h"
+#include "video.h"
+
+namespace {
+int invidiousFixedMax = 20;
+}
+
+IVSearch::IVSearch(SearchParams *searchParams, QObject *parent)
+    : VideoSource(parent), searchParams(searchParams) {
+    searchParams->setParent(this);
+}
+
+void IVSearch::loadVideos(int max, int startIndex) {
+    aborted = false;
+
+    QUrl url = Invidious::instance().method("search");
+
+    QUrlQuery q(url);
+
+    // invidious always returns 20 results
+    int page = ((startIndex - 1) / invidiousFixedMax) + 1;
+    q.addQueryItem("page", QString::number(page));
+
+    if (!searchParams->keywords().isEmpty()) {
+        q.addQueryItem("q", searchParams->keywords());
+    }
+
+    if (!searchParams->channelId().isEmpty())
+        q.addQueryItem("channelId", searchParams->channelId());
+
+    switch (searchParams->sortBy()) {
+    case SearchParams::SortByNewest:
+        q.addQueryItem("sort_by", "upload_date");
+        break;
+    case SearchParams::SortByViewCount:
+        q.addQueryItem("sort_by", "view_count");
+        break;
+    case SearchParams::SortByRating:
+        q.addQueryItem("sort_by", "rating");
+        break;
+    }
+
+    switch (searchParams->duration()) {
+    case SearchParams::DurationShort:
+        q.addQueryItem("duration", "short");
+        break;
+    case SearchParams::DurationMedium:
+        q.addQueryItem("duration", "medium");
+        break;
+    case SearchParams::DurationLong:
+        q.addQueryItem("duration", "long");
+        break;
+    }
+
+    switch (searchParams->time()) {
+    case SearchParams::TimeToday:
+        q.addQueryItem("date", "today");
+        break;
+    case SearchParams::TimeWeek:
+        q.addQueryItem("date", "week");
+        break;
+    case SearchParams::TimeMonth:
+        q.addQueryItem("date", "month");
+        break;
+    }
+
+    switch (searchParams->quality()) {
+    case SearchParams::QualityHD:
+        q.addQueryItem("features", "hd");
+        break;
+    }
+
+    url.setQuery(q);
+
+    // qWarning() << "YT3 search" << url.toString();
+    QObject *reply = Invidious::cachedHttp().get(url);
+    connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+    connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
+}
+
+void IVSearch::parseResults(const QByteArray &data) {
+    if (aborted) return;
+
+    QJsonDocument doc = QJsonDocument::fromJson(data);
+    const QJsonArray items = doc.array();
+    IVListParser parser(items);
+    const QVector<Video *> &videos = parser.getVideos();
+
+    if (items.size() > invidiousFixedMax) invidiousFixedMax = items.size();
+
+    if (name.isEmpty() && !searchParams->channelId().isEmpty()) {
+        if (!videos.isEmpty()) {
+            name = videos.at(0)->getChannelTitle();
+            if (!searchParams->keywords().isEmpty()) {
+                name += QLatin1String(": ") + searchParams->keywords();
+            }
+        }
+        emit nameChanged(name);
+    }
+
+    emit gotVideos(videos);
+    emit finished(videos.size());
+}
+
+void IVSearch::abort() {
+    aborted = true;
+}
+
+QString IVSearch::getName() {
+    if (!name.isEmpty()) return name;
+    if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
+    return QString();
+}
+
+void IVSearch::requestError(const QString &message) {
+    Invidious::instance().initServers();
+    QString msg = message;
+    emit error(msg);
+}
+
+const QList<QAction *> &IVSearch::getActions() {
+    static const QList<QAction *> channelActions = {
+            MainWindow::instance()->getAction("subscribeChannel")};
+    if (searchParams->channelId().isEmpty()) {
+        static const QList<QAction *> noActions;
+        return noActions;
+    }
+    return channelActions;
+}
+
+int IVSearch::maxResults() {
+    return invidiousFixedMax;
+}
diff --git a/src/invidious/ivsearch.h b/src/invidious/ivsearch.h
new file mode 100644 (file)
index 0000000..814877e
--- /dev/null
@@ -0,0 +1,32 @@
+#ifndef IVSEARCH_H
+#define IVSEARCH_H
+
+#include "videosource.h"
+#include <QtNetwork>
+
+class SearchParams;
+class Video;
+
+class IVSearch : public VideoSource {
+    Q_OBJECT
+
+public:
+    IVSearch(SearchParams *params, QObject *parent = 0);
+    void loadVideos(int max, int startIndex);
+    void abort();
+    QString getName();
+    const QList<QAction *> &getActions();
+    int maxResults();
+    SearchParams *getSearchParams() const { return searchParams; }
+
+private slots:
+    void parseResults(const QByteArray &data);
+    void requestError(const QString &message);
+
+private:
+    SearchParams *searchParams;
+    bool aborted;
+    QString name;
+};
+
+#endif // IVSEARCH_H
diff --git a/src/invidious/ivsinglevideosource.cpp b/src/invidious/ivsinglevideosource.cpp
new file mode 100644 (file)
index 0000000..ff0c804
--- /dev/null
@@ -0,0 +1,79 @@
+#include "ivsinglevideosource.h"
+
+#include "http.h"
+#include "httputils.h"
+#include "video.h"
+
+#include "invidious.h"
+#include "ivlistparser.h"
+
+IVSingleVideoSource::IVSingleVideoSource(QObject *parent)
+    : VideoSource(parent), video(nullptr), startIndex(0), max(0) {}
+
+void IVSingleVideoSource::loadVideos(int max, int startIndex) {
+    aborted = false;
+    this->startIndex = startIndex;
+    this->max = max;
+
+    QUrl url;
+
+    if (startIndex == 1) {
+        if (video) {
+            QVector<Video *> videos;
+            videos << video->clone();
+            if (name.isEmpty()) {
+                name = videos.at(0)->getTitle();
+                qDebug() << "Emitting name changed" << name;
+                emit nameChanged(name);
+            }
+            emit gotVideos(videos);
+            loadVideos(max - 1, 2);
+            return;
+        }
+
+        url = Invidious::instance().method("videos/");
+        url.setPath(url.path() + videoId);
+
+    } else {
+        url = Invidious::instance().method("videos");
+        url.setPath(url.path() + "/" + videoId);
+    }
+
+    QObject *reply = Invidious::cachedHttp().get(url);
+    connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+    connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
+}
+
+void IVSingleVideoSource::parseResults(QByteArray data) {
+    if (aborted) return;
+
+    QJsonDocument doc = QJsonDocument::fromJson(data);
+    const QJsonArray items = doc.object()["recommendedVideos"].toArray();
+    IVListParser parser(items);
+    const QVector<Video *> &videos = parser.getVideos();
+
+    emit gotVideos(videos);
+    if (startIndex == 1)
+        loadVideos(max - 1, 2);
+    else if (startIndex == 2)
+        emit finished(videos.size() + 1);
+    else
+        emit finished(videos.size());
+}
+
+void IVSingleVideoSource::abort() {
+    aborted = true;
+}
+
+QString IVSingleVideoSource::getName() {
+    return name;
+}
+
+void IVSingleVideoSource::setVideo(Video *video) {
+    this->video = video;
+    videoId = video->getId();
+}
+
+void IVSingleVideoSource::requestError(const QString &message) {
+    emit error(message);
+}
diff --git a/src/invidious/ivsinglevideosource.h b/src/invidious/ivsinglevideosource.h
new file mode 100644 (file)
index 0000000..c5431b9
--- /dev/null
@@ -0,0 +1,33 @@
+#ifndef IVSINGLEVIDEOSOURCE_H
+#define IVSINGLEVIDEOSOURCE_H
+
+#include <QtCore>
+
+#include "videosource.h"
+
+class IVSingleVideoSource : public VideoSource {
+    Q_OBJECT
+public:
+    IVSingleVideoSource(QObject *parent = 0);
+
+    void loadVideos(int max, int startIndex);
+    void abort();
+    QString getName();
+
+    void setVideoId(const QString &value) { videoId = value; }
+    void setVideo(Video *video);
+
+private slots:
+    void parseResults(QByteArray data);
+    void requestError(const QString &message);
+
+private:
+    Video *video;
+    QString videoId;
+    bool aborted;
+    int startIndex;
+    int max;
+    QString name;
+};
+
+#endif // IVSINGLEVIDEOSOURCE_H
diff --git a/src/invidious/ivvideolist.cpp b/src/invidious/ivvideolist.cpp
new file mode 100644 (file)
index 0000000..2ab47a6
--- /dev/null
@@ -0,0 +1,36 @@
+#include "ivvideolist.h"
+
+#include "http.h"
+#include "httputils.h"
+#include "invidious.h"
+#include "ivlistparser.h"
+#include "video.h"
+
+IVVideoList::IVVideoList(const QString &req, const QString &name, QObject *parent)
+    : VideoSource(parent), name(name), req(req) {}
+
+void IVVideoList::loadVideos(int max, int startIndex) {
+    aborted = false;
+
+    QUrl url(Invidious::instance().baseUrl() + req);
+
+    auto *reply = Invidious::cachedHttp().get(url);
+    connect(reply, &HttpReply::data, this, [this](auto data) {
+        QJsonDocument doc = QJsonDocument::fromJson(data);
+        const QJsonArray items = doc.array();
+        IVListParser parser(items);
+        const QVector<Video *> &videos = parser.getVideos();
+        qDebug() << "CAOCAO" << req << name << videos.size();
+
+        emit gotVideos(videos);
+        emit finished(videos.size());
+    });
+    connect(reply, &HttpReply::error, this, [this](auto message) {
+        Invidious::instance().initServers();
+        emit error(message);
+    });
+}
+
+void IVVideoList::abort() {
+    aborted = true;
+}
diff --git a/src/invidious/ivvideolist.h b/src/invidious/ivvideolist.h
new file mode 100644 (file)
index 0000000..34b091e
--- /dev/null
@@ -0,0 +1,24 @@
+#ifndef IVVIDEOLIST_H
+#define IVVIDEOLIST_H
+
+#include "videosource.h"
+#include <QtCore>
+
+class IVVideoList : public VideoSource {
+    Q_OBJECT
+
+public:
+    IVVideoList(const QString &req, const QString &name, QObject *parent = nullptr);
+
+    void loadVideos(int max, int startIndex);
+    void abort();
+    QString getName() { return name; };
+    bool hasMoreVideos() { return false; }
+
+private:
+    bool aborted;
+    QString name;
+    QString req;
+};
+
+#endif // IVVIDEOLIST_H
index 6332b1a7f407bf35f938145b1394b61d3d735e35..75e0a67eb91c563a2e891e5f52a2577ed0f9841f 100644 (file)
@@ -23,6 +23,8 @@ $END_LICENSE */
 
 #include "constants.h"
 #include "iconutils.h"
+#include "updateutils.h"
+
 #include "mainwindow.h"
 #include "searchparams.h"
 #include <qtsingleapplication.h>
@@ -140,6 +142,8 @@ int main(int argc, char **argv) {
 
     IconUtils::setSizes({16, 24, 32, 88});
 
+    UpdateUtils::init();
+
     showWindow(app, pkgDataDir);
 
     return app.exec();
index abf71e5642f63ffaa0b497ee5222a9fcbb2e53cb..df79a99443f7aaded1bfe773f47bab9ab7996318 100644 (file)
@@ -48,7 +48,6 @@ $END_LICENSE */
 #endif
 #include "downloadmanager.h"
 #include "temporary.h"
-#include "updatechecker.h"
 #include "ytsuggester.h"
 #if defined(APP_MAC_SEARCHFIELD) && !defined(APP_MAC_QMACTOOLBAR)
 #include "searchlineedit_mac.h"
@@ -79,6 +78,9 @@ $END_LICENSE */
 #include "yt3.h"
 #include "ytregions.h"
 
+#include "invidious.h"
+#include "videoapi.h"
+
 #ifdef MEDIA_QTAV
 #include "mediaqtav.h"
 #endif
@@ -86,6 +88,10 @@ $END_LICENSE */
 #include "mediampv.h"
 #endif
 
+#ifdef UPDATER
+#include "updater.h"
+#endif
+
 namespace {
 MainWindow *mainWindowInstance;
 }
@@ -157,6 +163,12 @@ MainWindow::MainWindow()
     showHome();
 #endif
 
+    if (VideoAPI::impl() == VideoAPI::IV) {
+        Invidious::instance().initServers();
+    } else if (VideoAPI::impl() == VideoAPI::YT3) {
+        YT3::instance().initApiKeys();
+    }
+
     QTimer::singleShot(100, this, &MainWindow::lazyInit);
 }
 
@@ -216,7 +228,9 @@ void MainWindow::lazyInit() {
 
     ChannelAggregator::instance()->start();
 
-    checkForUpdate();
+#ifdef UPDATER
+    Updater::instance().checkWithoutUI();
+#endif
 
     initialized = true;
 }
@@ -653,6 +667,7 @@ void MainWindow::createActions() {
     action->setStatusTip(tr("Hide videos that may contain inappropriate content"));
     action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_K));
     action->setCheckable(true);
+    action->setVisible(VideoAPI::impl() == VideoAPI::YT3);
     actionMap.insert("safeSearch", action);
 
     action = new QAction(tr("Toggle &Menu Bar"), this);
@@ -769,6 +784,9 @@ void MainWindow::createMenus() {
 #endif
     helpMenu->addAction(getAction("reportIssue"));
     helpMenu->addAction(aboutAct);
+#ifdef UPDATER
+    helpMenu->addAction(Updater::instance().getAction());
+#endif
 
 #ifdef APP_MAC_STORE
     helpMenu->addSeparator();
@@ -1789,53 +1807,6 @@ void MainWindow::dropEvent(QDropEvent *e) {
     }
 }
 
-void MainWindow::checkForUpdate() {
-    static const QString updateCheckKey = "updateCheck";
-
-    // check every 24h
-    QSettings settings;
-    uint unixTime = QDateTime::currentDateTime().toTime_t();
-    int lastCheck = settings.value(updateCheckKey).toInt();
-    int secondsSinceLastCheck = unixTime - lastCheck;
-    // qDebug() << "secondsSinceLastCheck" << unixTime << lastCheck << secondsSinceLastCheck;
-    if (secondsSinceLastCheck < 86400) return;
-
-    // check it out
-    UpdateChecker *updateChecker = new UpdateChecker();
-    connect(updateChecker, &UpdateChecker::newVersion, this,
-            [this, updateChecker](const QString &version) {
-                updateChecker->deleteLater();
-                QSettings settings;
-                QString checkedVersion = settings.value("checkedVersion").toString();
-                if (checkedVersion == version) return;
-#ifdef APP_SIMPLEUPDATE
-                simpleUpdateDialog(version);
-#elif defined(APP_EXTRA) && !defined(APP_MAC)
-                UpdateDialog *dialog = new UpdateDialog(version, this);
-                dialog->show();
-#endif
-            });
-    updateChecker->checkForUpdate();
-    settings.setValue(updateCheckKey, unixTime);
-}
-
-void MainWindow::simpleUpdateDialog(const QString &version) {
-    QMessageBox msgBox(this);
-    msgBox.setIconPixmap(IconUtils::pixmap(":/images/64x64/app.png", devicePixelRatioF()));
-    msgBox.setText(tr("%1 version %2 is now available.").arg(Constants::NAME, version));
-    msgBox.setModal(true);
-    msgBox.setWindowModality(Qt::WindowModal);
-    msgBox.addButton(QMessageBox::Close);
-    QPushButton *laterButton = msgBox.addButton(tr("Remind me later"), QMessageBox::RejectRole);
-    QPushButton *updateButton = msgBox.addButton(tr("Update"), QMessageBox::AcceptRole);
-    msgBox.exec();
-    if (msgBox.clickedButton() != laterButton) {
-        QSettings settings;
-        settings.setValue("checkedVersion", version);
-    }
-    if (msgBox.clickedButton() == updateButton) visitSite();
-}
-
 bool MainWindow::needStatusBar() {
     return !statusToolBar->actions().isEmpty();
 }
index e7d80b67f2e5857dc8028347c69a4dfbe1a6ba3c..5a71261bbe5ab14e570b5ebfbe4d9d6ce49d3e35 100644 (file)
@@ -111,7 +111,6 @@ protected:
 
 private slots:
     void lazyInit();
-    void checkForUpdate();
     void donate();
     void reportIssue();
     void about();
@@ -159,7 +158,6 @@ private:
     void showView(View *view, bool transition = false);
     static QString formatTime(qint64 duration);
     bool confirmQuit();
-    void simpleUpdateDialog(const QString &version);
     bool needStatusBar();
     void adjustMessageLabelPosition();
 
index 46eb8ead94571fd0d95036c2b01245daa086ff51..bf60db1d42aeddf1cdbff337c5aa8b6357f93ac8 100644 (file)
@@ -53,6 +53,11 @@ $END_LICENSE */
 #include "idle.h"
 #include "videodefinition.h"
 
+#include "ivchannelsource.h"
+#include "ivsearch.h"
+#include "ivsinglevideosource.h"
+#include "videoapi.h"
+
 MediaView *MediaView::instance() {
     static MediaView *i = new MediaView();
     return i;
@@ -218,8 +223,18 @@ void MediaView::setMedia(Media *media) {
 
 SearchParams *MediaView::getSearchParams() {
     VideoSource *videoSource = playlistModel->getVideoSource();
-    if (videoSource && videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
-        YTSearch *search = qobject_cast<YTSearch *>(videoSource);
+    if (!videoSource) return nullptr;
+    auto clazz = videoSource->metaObject()->className();
+    if (clazz == QLatin1String("YTSearch")) {
+        auto search = qobject_cast<YTSearch *>(videoSource);
+        return search->getSearchParams();
+    }
+    if (clazz == QLatin1String("IVSearch")) {
+        auto search = qobject_cast<IVSearch *>(videoSource);
+        return search->getSearchParams();
+    }
+    if (clazz == QLatin1String("IVChannelSource")) {
+        auto search = qobject_cast<IVChannelSource *>(videoSource);
         return search->getSearchParams();
     }
     return nullptr;
@@ -231,19 +246,39 @@ void MediaView::search(SearchParams *searchParams) {
             searchParams->keywords().startsWith("https://")) {
             QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
             if (!videoId.isEmpty()) {
-                YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource(this);
-                singleVideoSource->setVideoId(videoId);
+                VideoSource *singleVideoSource = nullptr;
+                if (VideoAPI::impl() == VideoAPI::YT3) {
+                    auto source = new YTSingleVideoSource(this);
+                    source->setVideoId(videoId);
+                    singleVideoSource = source;
+                } else if (VideoAPI::impl() == VideoAPI::IV) {
+                    auto source = new IVSingleVideoSource(this);
+                    source->setVideoId(videoId);
+                    singleVideoSource = source;
+                }
                 setVideoSource(singleVideoSource);
+
                 QTime tstamp = YTSearch::videoTimestampFromUrl(searchParams->keywords());
                 pauseTime = QTime(0, 0).msecsTo(tstamp);
                 return;
             }
         }
     }
-    YTSearch *ytSearch = new YTSearch(searchParams);
-    ytSearch->setAsyncDetails(true);
-    connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
-    setVideoSource(ytSearch);
+
+    VideoSource *search = nullptr;
+    if (VideoAPI::impl() == VideoAPI::YT3) {
+        YTSearch *ytSearch = new YTSearch(searchParams);
+        ytSearch->setAsyncDetails(true);
+        connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
+        search = ytSearch;
+    } else if (VideoAPI::impl() == VideoAPI::IV) {
+        if (searchParams->channelId().isEmpty()) {
+            search = new IVSearch(searchParams);
+        } else {
+            search = new IVChannelSource(searchParams);
+        }
+    }
+    setVideoSource(search);
 }
 
 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
@@ -253,13 +288,6 @@ void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool
 
     // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
 
-    YTSearch * ytSearch = qobject_cast<YTSearch *>(videoSource);
-    if (nullptr != ytSearch) {
-        if (!ytSearch->getSearchParams()->channelId().isEmpty()) {
-            updateSubscriptionActionForChannel(ytSearch->getSearchParams()->channelId());
-        }
-    }
-
     if (addToHistory) {
         int currentIndex = getHistoryIndex();
         if (currentIndex >= 0 && currentIndex < history.size() - 1) {
@@ -288,13 +316,17 @@ void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool
         }
     }
 
+    SearchParams *searchParams = getSearchParams();
+
     sidebar->showPlaylist();
-    sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
+    sidebar->getRefineSearchWidget()->setSearchParams(searchParams);
     sidebar->hideSuggestions();
     sidebar->getHeader()->updateInfo();
 
-    SearchParams *searchParams = getSearchParams();
     bool isChannel = searchParams && !searchParams->channelId().isEmpty();
+    if (isChannel) {
+        updateSubscriptionActionForChannel(searchParams->channelId());
+    }
     playlistView->setClickableAuthors(!isChannel);
 }
 
@@ -901,10 +933,18 @@ void MediaView::findVideoParts() {
 void MediaView::relatedVideos() {
     Video *video = playlistModel->activeVideo();
     if (!video) return;
-    YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
-    singleVideoSource->setVideo(video->clone());
-    singleVideoSource->setAsyncDetails(true);
-    setVideoSource(singleVideoSource);
+
+    if (VideoAPI::impl() == VideoAPI::YT3) {
+        YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
+        singleVideoSource->setVideo(video->clone());
+        singleVideoSource->setAsyncDetails(true);
+        setVideoSource(singleVideoSource);
+    } else if (VideoAPI::impl() == VideoAPI::IV) {
+        auto source = new IVSingleVideoSource(this);
+        source->setVideo(video->clone());
+        setVideoSource(source);
+    }
+
     MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
 }
 
index 159eeae7c819ad7db5aeab7bc0ee3efb460efc26..2a9f176797f6e8100cc1ac2e131e38de7ac8dd54 100644 (file)
@@ -8,7 +8,7 @@ MessageBar::MessageBar(QWidget *parent) : QWidget(parent) {
     layout->setSpacing(16);
 
     msgLabel = new QLabel();
-    msgLabel->setOpenExternalLinks(true);
+    connect(msgLabel, &QLabel::linkActivated, this, &MessageBar::linkActivated);
     layout->addWidget(msgLabel);
 
     QToolButton *closeToolButton = new QToolButton();
@@ -24,6 +24,10 @@ void MessageBar::setMessage(const QString &message) {
     msgLabel->setText(message);
 }
 
+void MessageBar::setOpenExternalLinks(bool value) {
+    msgLabel->setOpenExternalLinks(value);
+}
+
 void MessageBar::paintEvent(QPaintEvent *e) {
     Q_UNUSED(e);
     QStyleOption o;
index 34c4d06bede5b2190068f64df8e963d60ef0d7c7..edceb6114149989fb3ffc65c84beba8efa60893f 100644 (file)
@@ -9,8 +9,10 @@ class MessageBar : public QWidget {
 public:
     MessageBar(QWidget *parent = 0);
     void setMessage(const QString &message);
+    void setOpenExternalLinks(bool value);
 
 signals:
+    void linkActivated(const QString &link);
     void closed();
 
 protected:
index c4a7d72b9fdfd096491c56de9ffa870fc4c8902e..99083489b18356919e86058779abcb804ebb66b2 100644 (file)
@@ -24,10 +24,12 @@ $END_LICENSE */
 #include "video.h"
 #include "videomimedata.h"
 #include "videosource.h"
+
+#include "ivchannelsource.h"
+#include "ivsearch.h"
 #include "ytsearch.h"
 
 namespace {
-const int maxItems = 50;
 const QString recentKeywordsKey = "recentKeywords";
 const QString recentChannelsKey = "recentChannels";
 } // namespace
@@ -186,27 +188,25 @@ void PlaylistModel::setVideoSource(VideoSource *videoSource) {
             },
             Qt::UniqueConnection);
 
+    canSearchMore = true;
     searchMore();
 }
 
-void PlaylistModel::searchMore(int max) {
-    if (videoSource == nullptr || searching) return;
+void PlaylistModel::searchMore() {
+    if (!canSearchMore || videoSource == nullptr || searching) return;
     searching = true;
     firstSearch = startIndex == 1;
-    this->max = max;
+    max = videoSource->maxResults();
+    if (max == 0) max = 20;
     errorMessage.clear();
     videoSource->loadVideos(max, startIndex);
     startIndex += max;
 }
 
-void PlaylistModel::searchMore() {
-    searchMore(maxItems);
-}
-
 void PlaylistModel::searchNeeded() {
     const int desiredRowsAhead = 10;
     int remainingRows = videos.size() - m_activeRow;
-    if (remainingRows < desiredRowsAhead) searchMore(maxItems);
+    if (remainingRows < desiredRowsAhead) searchMore();
 }
 
 void PlaylistModel::abortSearch() {
@@ -229,7 +229,7 @@ void PlaylistModel::searchFinished(int total) {
     canSearchMore = videoSource->hasMoreVideos();
 
     // update the message item
-    emit dataChanged(createIndex(maxItems, 0), createIndex(maxItems, columnCount() - 1));
+    emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
 
     if (firstSearch && !videos.isEmpty()) handleFirstVideo(videos.at(0));
 }
@@ -237,7 +237,7 @@ void PlaylistModel::searchFinished(int total) {
 void PlaylistModel::searchError(const QString &message) {
     errorMessage = message;
     // update the message item
-    emit dataChanged(createIndex(maxItems, 0), createIndex(maxItems, columnCount() - 1));
+    emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
 }
 
 void PlaylistModel::addVideos(const QVector<Video *> &newVideos) {
@@ -261,11 +261,22 @@ void PlaylistModel::handleFirstVideo(Video *video) {
         if (!settings.value("manualplay", false).toBool()) setActiveRow(0);
     }
 
-    if (videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
+    auto clazz = videoSource->metaObject()->className();
+    if (clazz == QLatin1String("YTSearch") || clazz == QLatin1String("IVSearch") ||
+        clazz == QLatin1String("IVChannelSource")) {
         static const int maxRecentElements = 10;
 
-        YTSearch *search = qobject_cast<YTSearch *>(videoSource);
-        SearchParams *searchParams = search->getSearchParams();
+        SearchParams *searchParams;
+        if (clazz == QLatin1String("YTSearch")) {
+            auto search = qobject_cast<YTSearch *>(videoSource);
+            searchParams = search->getSearchParams();
+        } else if (clazz == QLatin1String("IVSearch")) {
+            auto search = qobject_cast<IVSearch *>(videoSource);
+            searchParams = search->getSearchParams();
+        } else if (clazz == QLatin1String("IVChannelSource")) {
+            auto search = qobject_cast<IVChannelSource *>(videoSource);
+            searchParams = search->getSearchParams();
+        }
 
         // save keyword
         QString query = searchParams->keywords();
index a510e6f5ed58896506dd30f28b18f51de2cdcfce..2cba04a89a55b1954d559541763f5942d98ba0a3 100644 (file)
@@ -108,7 +108,6 @@ signals:
 
 private:
     void handleFirstVideo(Video *video);
-    void searchMore(int max);
 
     VideoSource *videoSource;
     bool searching;
index c13a8301348a82ec9ab1ca26a9738c35c52f0b57..b504ac800e7a5c7365855b41b6089e75193bd018 100644 (file)
@@ -45,6 +45,10 @@ $END_LICENSE */
 #include "messagebar.h"
 #include "painterutils.h"
 
+#ifdef UPDATER
+#include "updater.h"
+#endif
+
 namespace {
 const QString recentKeywordsKey = "recentKeywords";
 const QString recentChannelsKey = "recentChannels";
@@ -468,8 +472,10 @@ void SearchView::maybeShowMessage() {
     if (showMessages && !settings.contains(key = "sofa")) {
         QString msg = tr("Need a remote control for %1? Try %2!").arg(Constants::NAME).arg("Sofa");
         msg = "<a href='https://" + QLatin1String(Constants::ORG_DOMAIN) + '/' + key +
-              "' style = 'text-decoration:none;color:palette(windowText)' > " + msg + "</a>";
+              "' style = 'text-decoration:none;color:palette(windowText)'>" + msg + "</a>";
         messageBar->setMessage(msg);
+        messageBar->setOpenExternalLinks(true);
+        disconnect(messageBar);
         connect(messageBar, &MessageBar::closed, this, [key] {
             QSettings settings;
             settings.setValue(key, true);
@@ -492,9 +498,10 @@ void SearchView::maybeShowMessage() {
                         tr("I keep improving %1 to make it the best I can. Support this work!")
                                 .arg(Constants::NAME);
                 msg = "<a href='https://" + QLatin1String(Constants::ORG_DOMAIN) + "/donate" +
-                      "' style = 'text-decoration:none;color:palette(windowText)' > " + msg +
-                      "</a>";
+                      "' style = 'text-decoration:none;color:palette(windowText)'>" + msg + "</a>";
                 messageBar->setMessage(msg);
+                messageBar->setOpenExternalLinks(true);
+                disconnect(messageBar);
                 connect(messageBar, &MessageBar::closed, this, [key] {
                     QSettings settings;
                     settings.setValue(key, true);
@@ -503,4 +510,20 @@ void SearchView::maybeShowMessage() {
             }
         }
     }
+
+#ifdef UPDATER
+    connect(&Updater::instance(), &Updater::statusChanged, this, [this](auto status) {
+        if (status == Updater::Status::UpdateDownloaded) {
+            QString msg = tr("An update is ready to be installed. Quit and install update.");
+            msg = "<a href='http://quit' style = "
+                  "'text-decoration:none;color:palette(windowText)'>" +
+                  msg + "</a>";
+            messageBar->setMessage(msg);
+            messageBar->setOpenExternalLinks(false);
+            disconnect(messageBar);
+            connect(messageBar, &MessageBar::linkActivated, this, [] { qApp->quit(); });
+            messageBar->show();
+        }
+    });
+#endif
 }
index 9080f1ce0be2beef85219a91bca750f1465f06e7..232b4d5f20d2923643f36ad02df41adfd63fbb66 100644 (file)
@@ -26,6 +26,9 @@ $END_LICENSE */
 #include "ytregions.h"
 #include "ytstandardfeed.h"
 
+#include "ivvideolist.h"
+#include "videoapi.h"
+
 StandardFeedsView::StandardFeedsView(QWidget *parent) : View(parent), layout(0) {
     setBackgroundRole(QPalette::Base);
     setAutoFillBackground(true);
@@ -39,16 +42,27 @@ StandardFeedsView::StandardFeedsView(QWidget *parent) : View(parent), layout(0)
 
 void StandardFeedsView::load() {
     setUpdatesEnabled(false);
-    YTCategories *youTubeCategories = new YTCategories(this);
-    connect(youTubeCategories, SIGNAL(categoriesLoaded(const QVector<YTCategory> &)),
-            SLOT(layoutCategories(const QVector<YTCategory> &)));
-    youTubeCategories->loadCategories();
-
     resetLayout();
 
-    addVideoSourceWidget(buildStandardFeed("most_popular", tr("Most Popular")));
-
     YTRegion region = YTRegions::currentRegion();
+
+    if (VideoAPI::impl() == VideoAPI::YT3) {
+        YTCategories *youTubeCategories = new YTCategories(this);
+        connect(youTubeCategories, SIGNAL(categoriesLoaded(const QVector<YTCategory> &)),
+                SLOT(layoutCategories(const QVector<YTCategory> &)));
+        youTubeCategories->loadCategories();
+        addVideoSourceWidget(buildStandardFeed("most_popular", tr("Most Popular")));
+    } else if (VideoAPI::impl() == VideoAPI::IV) {
+        QString regionParam = "region=" + region.id;
+        addVideoSourceWidget(new IVVideoList("popular?" + regionParam, tr("Most Popular")));
+        addVideoSourceWidget(new IVVideoList("trending?" + regionParam, tr("Trending")));
+        addVideoSourceWidget(new IVVideoList("trending?type=music&" + regionParam, tr("Music")));
+        addVideoSourceWidget(new IVVideoList("trending?type=news&" + regionParam, tr("News")));
+        addVideoSourceWidget(new IVVideoList("trending?type=movies&" + regionParam, tr("Movies")));
+        addVideoSourceWidget(new IVVideoList("trending?type=gaming&" + regionParam, tr("Gaming")));
+        setUpdatesEnabled(true);
+    }
+
     QAction *regionAction = MainWindow::instance()->getRegionAction();
     regionAction->setText(region.name);
     regionAction->setIcon(YTRegions::iconForRegionId(region.id));
@@ -74,7 +88,7 @@ void StandardFeedsView::addVideoSourceWidget(VideoSource *videoSource) {
     connect(w, SIGNAL(unavailable(VideoSourceWidget *)),
             SLOT(removeVideoSourceWidget(VideoSourceWidget *)));
     int i = layout->count();
-    const int cols = 5;
+    const int cols = VideoAPI::impl() == VideoAPI::YT3 ? 5 : 3;
     layout->addWidget(w, i / cols, i % cols);
 }
 
diff --git a/src/updatechecker.cpp b/src/updatechecker.cpp
deleted file mode 100644 (file)
index 07b3335..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-/* $BEGIN_LICENSE
-
-This file is part of Minitube.
-Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
-
-Minitube is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Minitube is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
-
-$END_LICENSE */
-
-#include "updatechecker.h"
-#include "http.h"
-#include "httputils.h"
-#include "constants.h"
-#ifdef APP_ACTIVATION
-#include "activation.h"
-#endif
-
-UpdateChecker::UpdateChecker() {
-    m_needUpdate = false;
-}
-
-void UpdateChecker::checkForUpdate() {
-    QUrl url(QLatin1String(Constants::WEBSITE) + "-ws/release.xml");
-    QUrlQuery q;
-    q.addQueryItem("v", Constants::VERSION);
-#ifdef APP_MAC
-    q.addQueryItem("os", "mac");
-#endif
-#ifdef APP_WIN
-    q.addQueryItem("os", "win");
-#endif
-#ifdef APP_ACTIVATION
-    QString t = "demo";
-    if (Activation::instance().isActivated()) t = "active";
-    q.addQueryItem("t", t);
-#endif
-#ifdef APP_MAC_STORE
-    q.addQueryItem("store", "mac");
-#endif
-    url.setQuery(q);
-
-    QObject *reply = HttpUtils::notCached().get(url);
-    connect(reply, SIGNAL(data(QByteArray)), SLOT(requestFinished(QByteArray)));
-}
-
-void UpdateChecker::requestFinished(QByteArray data) {
-    UpdateCheckerStreamReader reader;
-    reader.read(data);
-    m_needUpdate = reader.needUpdate();
-    m_remoteVersion = reader.remoteVersion();
-    if (m_needUpdate && !m_remoteVersion.isEmpty()) emit newVersion(m_remoteVersion);
-}
-
-QString UpdateChecker::remoteVersion() {
-    return m_remoteVersion;
-}
-
-// --- Reader ---
-
-bool UpdateCheckerStreamReader::read(QByteArray data) {
-    addData(data);
-
-    while (!atEnd()) {
-        readNext();
-        if (isStartElement()) {
-            if (name() == QLatin1String("release")) {
-                while (!atEnd()) {
-                    readNext();
-                    if (isStartElement() && name() == QLatin1String("version")) {
-                        QString remoteVersion = readElementText();
-                        qDebug() << remoteVersion << QString(Constants::VERSION);
-                        m_needUpdate = remoteVersion != QString(Constants::VERSION);
-                        m_remoteVersion = remoteVersion;
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    return !error();
-}
-
-QString UpdateCheckerStreamReader::remoteVersion() {
-    return m_remoteVersion;
-}
diff --git a/src/updatechecker.h b/src/updatechecker.h
deleted file mode 100644 (file)
index 74f0fb9..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/* $BEGIN_LICENSE
-
-This file is part of Minitube.
-Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
-
-Minitube is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Minitube is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
-
-$END_LICENSE */
-
-#ifndef UPDATECHECKER_H
-#define UPDATECHECKER_H
-
-#include <QXmlStreamReader>
-#include <QNetworkReply>
-
-class UpdateChecker : public QObject {
-    Q_OBJECT
-
-public:
-    UpdateChecker();
-    void checkForUpdate();
-    QString remoteVersion();
-
-signals:
-    void newVersion(QString);
-
-private slots:
-    void requestFinished(QByteArray);
-
-private:
-
-    bool m_needUpdate;
-    QString m_remoteVersion;
-    QNetworkReply *networkReply;
-
-};
-
-class UpdateCheckerStreamReader : public QXmlStreamReader {
-
-public:
-    bool read(QByteArray data);
-    QString remoteVersion();
-    bool needUpdate() { return m_needUpdate; }
-
-private:
-    QString m_remoteVersion;
-    bool m_needUpdate;
-
-};
-
-#endif // UPDATECHECKER_H
diff --git a/src/updateutils.cpp b/src/updateutils.cpp
new file mode 100644 (file)
index 0000000..e89f400
--- /dev/null
@@ -0,0 +1,58 @@
+#include "updateutils.h"
+
+#include <QtCore>
+
+#include "constants.h"
+#include "iconutils.h"
+#include "mainwindow.h"
+
+#ifdef UPDATER
+#include "updater.h"
+#ifdef UPDATER_SPARKLE
+#include "sparkleupdater.h"
+#else
+#include "defaultupdater.h"
+#include "runinstaller.h"
+#include "simplexmlparser.h"
+#endif
+#endif
+
+namespace UpdateUtils {
+
+void init() {
+#ifdef UPDATER
+
+#ifdef UPDATER_SPARKLE
+    Updater::setInstance(new updater::SparkleUpdater());
+#else
+    auto updater = new updater::DefaultUpdater();
+
+    QUrl manifestUrl(QLatin1String(Constants::WEBSITE) + "-ws/release.xml");
+    updater->setManifestUrl(manifestUrl);
+    updater->setParser(new updater::SimpleXmlParser());
+
+    QString ext;
+#ifdef APP_MAC
+    ext = ".dmg";
+#elif defined APP_WIN
+    ext = ".exe";
+#else
+    ext = ".deb";
+#endif
+    QUrl downloadUrl("https://" + QLatin1String(Constants::ORG_DOMAIN) + "/files/" +
+                     Constants::UNIX_NAME + "/" + Constants::UNIX_NAME + ext);
+    updater->setDownloadUrl(downloadUrl);
+
+    auto installer = new updater::RunInstaller;
+#ifdef APP_WIN
+    installer->setArguments({"/S"});
+#endif
+    updater->setInstaller(installer);
+
+    Updater::setInstance(updater);
+#endif
+
+#endif
+}
+
+} // namespace UpdateUtils
diff --git a/src/updateutils.h b/src/updateutils.h
new file mode 100644 (file)
index 0000000..c1718ae
--- /dev/null
@@ -0,0 +1,10 @@
+#ifndef UPDATEUTILS_H
+#define UPDATEUTILS_H
+
+namespace UpdateUtils {
+
+void init();
+
+};
+
+#endif // UPDATEUTILS_H
diff --git a/src/videoapi.h b/src/videoapi.h
new file mode 100644 (file)
index 0000000..5a9f482
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef VIDEOAPI_H
+#define VIDEOAPI_H
+
+#include <QtCore>
+
+class VideoAPI {
+public:
+    enum Impl { YT3, IV };
+    static Impl impl() { return IV; }
+
+private:
+    VideoAPI() {}
+};
+
+#endif // VIDEOAPI_H
index eb93ec09066da7fc0145e94b446f48dee752ab8a..82a1eb6f98cfe44bf5e10630bd033d6a9f06ef66 100644 (file)
@@ -39,6 +39,7 @@ public:
         static const QList<QAction *> noActions;
         return noActions;
     }
+    virtual int maxResults() { return 0; }
 
 public slots:
     void setParam(const QString &name, const QVariant &value);
index 0e39560fbc24ca7906eaca2334bf85797579189b..a2eacf7eb540c81c7743ebd89f2a0b5fc8d7340c 100644 (file)
@@ -45,6 +45,7 @@ void VideoSourceWidget::activate() {
 void VideoSourceWidget::previewVideo(const QVector<Video *> &videos) {
     videoSource->disconnect();
     if (videos.isEmpty()) {
+        qDebug() << "Unavailable video source" << videoSource->getName();
         emit unavailable(this);
         return;
     }
@@ -52,7 +53,7 @@ void VideoSourceWidget::previewVideo(const QVector<Video *> &videos) {
     lastPixelRatio = window()->devicePixelRatio();
     bool needLargeThumb = lastPixelRatio > 1.0 || window()->width() > 1000;
     QString url =  needLargeThumb ? video->getLargeThumbnailUrl() : video->getMediumThumbnailUrl();
-    if (url.isEmpty()) url = video->getMediumThumbnailUrl();
+    if (url.isEmpty()) url = video->getThumbnailUrl();
     video->deleteLater();
     QObject *reply = HttpUtils::yt().get(url);
     connect(reply, SIGNAL(data(QByteArray)), SLOT(setPixmapData(QByteArray)));
diff --git a/src/waitingspinnerwidget.cpp b/src/waitingspinnerwidget.cpp
new file mode 100644 (file)
index 0000000..0ef03a0
--- /dev/null
@@ -0,0 +1,277 @@
+/* Original Work Copyright (c) 2012-2014 Alexander Turkin
+   Modified 2014 by William Hallatt
+   Modified 2015 by Jacob Dawid
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+// Own includes
+#include "waitingspinnerwidget.h"
+
+// Standard includes
+#include <cmath>
+#include <algorithm>
+
+// Qt includes
+#include <QPainter>
+#include <QTimer>
+
+WaitingSpinnerWidget::WaitingSpinnerWidget(QWidget *parent,
+                                           bool centerOnParent,
+                                           bool disableParentWhenSpinning)
+    : QWidget(parent),
+      _centerOnParent(centerOnParent),
+      _disableParentWhenSpinning(disableParentWhenSpinning) {
+    initialize();
+}
+
+WaitingSpinnerWidget::WaitingSpinnerWidget(Qt::WindowModality modality,
+                                           QWidget *parent,
+                                           bool centerOnParent,
+                                           bool disableParentWhenSpinning)
+    : QWidget(parent, Qt::Dialog | Qt::FramelessWindowHint),
+      _centerOnParent(centerOnParent),
+      _disableParentWhenSpinning(disableParentWhenSpinning){
+    initialize();
+
+    // We need to set the window modality AFTER we've hidden the
+    // widget for the first time since changing this property while
+    // the widget is visible has no effect.
+    setWindowModality(modality);
+    setAttribute(Qt::WA_TranslucentBackground);
+}
+
+void WaitingSpinnerWidget::initialize() {
+    _color = Qt::black;
+    _roundness = 100.0;
+    _minimumTrailOpacity = 3.14159265358979323846;
+    _trailFadePercentage = 80.0;
+    _revolutionsPerSecond = 1.57079632679489661923;
+    _numberOfLines = 20;
+    _lineLength = 10;
+    _lineWidth = 2;
+    _innerRadius = 10;
+    _currentCounter = 0;
+    _isSpinning = false;
+
+    _timer = new QTimer(this);
+    connect(_timer, SIGNAL(timeout()), this, SLOT(rotate()));
+    updateSize();
+    updateTimer();
+    hide();
+}
+
+void WaitingSpinnerWidget::paintEvent(QPaintEvent *) {
+    updatePosition();
+    QPainter painter(this);
+    painter.fillRect(this->rect(), Qt::transparent);
+    painter.setRenderHint(QPainter::Antialiasing, true);
+
+    if (_currentCounter >= _numberOfLines) {
+        _currentCounter = 0;
+    }
+
+    painter.setPen(Qt::NoPen);
+    for (int i = 0; i < _numberOfLines; ++i) {
+        painter.save();
+        painter.translate(_innerRadius + _lineLength,
+                          _innerRadius + _lineLength);
+        qreal rotateAngle =
+                static_cast<qreal>(360 * i) / static_cast<qreal>(_numberOfLines);
+        painter.rotate(rotateAngle);
+        painter.translate(_innerRadius, 0);
+        int distance =
+                lineCountDistanceFromPrimary(i, _currentCounter, _numberOfLines);
+        QColor color =
+                currentLineColor(distance, _numberOfLines, _trailFadePercentage,
+                                 _minimumTrailOpacity, _color);
+        painter.setBrush(color);
+        // TODO improve the way rounded rect is painted
+        painter.drawRoundedRect(
+                    QRect(0, -_lineWidth / 2, _lineLength, _lineWidth), _roundness,
+                    _roundness, Qt::RelativeSize);
+        painter.restore();
+    }
+}
+
+void WaitingSpinnerWidget::start() {
+    updatePosition();
+    _isSpinning = true;
+    show();
+
+    if(parentWidget() && _disableParentWhenSpinning) {
+        parentWidget()->setEnabled(false);
+    }
+
+    if (!_timer->isActive()) {
+        _timer->start();
+        _currentCounter = 0;
+    }
+}
+
+void WaitingSpinnerWidget::stop() {
+    _isSpinning = false;
+    hide();
+
+    if(parentWidget() && _disableParentWhenSpinning) {
+        parentWidget()->setEnabled(true);
+    }
+
+    if (_timer->isActive()) {
+        _timer->stop();
+        _currentCounter = 0;
+    }
+}
+
+void WaitingSpinnerWidget::setNumberOfLines(int lines) {
+    _numberOfLines = lines;
+    _currentCounter = 0;
+    updateTimer();
+}
+
+void WaitingSpinnerWidget::setLineLength(int length) {
+    _lineLength = length;
+    updateSize();
+}
+
+void WaitingSpinnerWidget::setLineWidth(int width) {
+    _lineWidth = width;
+    updateSize();
+}
+
+void WaitingSpinnerWidget::setInnerRadius(int radius) {
+    _innerRadius = radius;
+    updateSize();
+}
+
+QColor WaitingSpinnerWidget::color() {
+    return _color;
+}
+
+qreal WaitingSpinnerWidget::roundness() {
+    return _roundness;
+}
+
+qreal WaitingSpinnerWidget::minimumTrailOpacity() {
+    return _minimumTrailOpacity;
+}
+
+qreal WaitingSpinnerWidget::trailFadePercentage() {
+    return _trailFadePercentage;
+}
+
+qreal WaitingSpinnerWidget::revolutionsPersSecond() {
+    return _revolutionsPerSecond;
+}
+
+int WaitingSpinnerWidget::numberOfLines() {
+    return _numberOfLines;
+}
+
+int WaitingSpinnerWidget::lineLength() {
+    return _lineLength;
+}
+
+int WaitingSpinnerWidget::lineWidth() {
+    return _lineWidth;
+}
+
+int WaitingSpinnerWidget::innerRadius() {
+    return _innerRadius;
+}
+
+bool WaitingSpinnerWidget::isSpinning() const {
+    return _isSpinning;
+}
+
+void WaitingSpinnerWidget::setRoundness(qreal roundness) {
+    _roundness = std::max(0.0, std::min(100.0, roundness));
+}
+
+void WaitingSpinnerWidget::setColor(QColor color) {
+    _color = color;
+}
+
+void WaitingSpinnerWidget::setRevolutionsPerSecond(qreal revolutionsPerSecond) {
+    _revolutionsPerSecond = revolutionsPerSecond;
+    updateTimer();
+}
+
+void WaitingSpinnerWidget::setTrailFadePercentage(qreal trail) {
+    _trailFadePercentage = trail;
+}
+
+void WaitingSpinnerWidget::setMinimumTrailOpacity(qreal minimumTrailOpacity) {
+    _minimumTrailOpacity = minimumTrailOpacity;
+}
+
+void WaitingSpinnerWidget::rotate() {
+    ++_currentCounter;
+    if (_currentCounter >= _numberOfLines) {
+        _currentCounter = 0;
+    }
+    update();
+}
+
+void WaitingSpinnerWidget::updateSize() {
+    int size = (_innerRadius + _lineLength) * 2;
+    setFixedSize(size, size);
+}
+
+void WaitingSpinnerWidget::updateTimer() {
+    _timer->setInterval(1000 / (_numberOfLines * _revolutionsPerSecond));
+}
+
+void WaitingSpinnerWidget::updatePosition() {
+    if (parentWidget() && _centerOnParent) {
+        move(parentWidget()->width() / 2 - width() / 2,
+             parentWidget()->height() / 2 - height() / 2);
+    }
+}
+
+int WaitingSpinnerWidget::lineCountDistanceFromPrimary(int current, int primary,
+                                                       int totalNrOfLines) {
+    int distance = primary - current;
+    if (distance < 0) {
+        distance += totalNrOfLines;
+    }
+    return distance;
+}
+
+QColor WaitingSpinnerWidget::currentLineColor(int countDistance, int totalNrOfLines,
+                                              qreal trailFadePerc, qreal minOpacity,
+                                              QColor color) {
+    if (countDistance == 0) {
+        return color;
+    }
+    const qreal minAlphaF = minOpacity / 100.0;
+    int distanceThreshold =
+            static_cast<int>(ceil((totalNrOfLines - 1) * trailFadePerc / 100.0));
+    if (countDistance > distanceThreshold) {
+        color.setAlphaF(minAlphaF);
+    } else {
+        qreal alphaDiff = color.alphaF() - minAlphaF;
+        qreal gradient = alphaDiff / static_cast<qreal>(distanceThreshold + 1);
+        qreal resultAlpha = color.alphaF() - gradient * countDistance;
+
+        // If alpha is out of bounds, clip it.
+        resultAlpha = std::min(1.0, std::max(0.0, resultAlpha));
+        color.setAlphaF(resultAlpha);
+    }
+    return color;
+}
diff --git a/src/waitingspinnerwidget.h b/src/waitingspinnerwidget.h
new file mode 100644 (file)
index 0000000..8129718
--- /dev/null
@@ -0,0 +1,114 @@
+/* Original Work Copyright (c) 2012-2014 Alexander Turkin
+   Modified 2014 by William Hallatt
+   Modified 2015 by Jacob Dawid
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+#pragma once
+
+// Qt includes
+#include <QWidget>
+#include <QTimer>
+#include <QColor>
+
+class WaitingSpinnerWidget : public QWidget {
+    Q_OBJECT
+public:
+    /*! Constructor for "standard" widget behaviour - use this
+   * constructor if you wish to, e.g. embed your widget in another. */
+    WaitingSpinnerWidget(QWidget *parent = 0,
+                         bool centerOnParent = true,
+                         bool disableParentWhenSpinning = true);
+
+    /*! Constructor - use this constructor to automatically create a modal
+   * ("blocking") spinner on top of the calling widget/window.  If a valid
+   * parent widget is provided, "centreOnParent" will ensure that
+   * QtWaitingSpinner automatically centres itself on it, if not,
+   * "centreOnParent" is ignored. */
+    WaitingSpinnerWidget(Qt::WindowModality modality,
+                         QWidget *parent = 0,
+                         bool centerOnParent = true,
+                         bool disableParentWhenSpinning = true);
+
+public slots:
+    void start();
+    void stop();
+
+public:
+    void setColor(QColor color);
+    void setRoundness(qreal roundness);
+    void setMinimumTrailOpacity(qreal minimumTrailOpacity);
+    void setTrailFadePercentage(qreal trail);
+    void setRevolutionsPerSecond(qreal revolutionsPerSecond);
+    void setNumberOfLines(int lines);
+    void setLineLength(int length);
+    void setLineWidth(int width);
+    void setInnerRadius(int radius);
+    void setText(QString text);
+
+    QColor color();
+    qreal roundness();
+    qreal minimumTrailOpacity();
+    qreal trailFadePercentage();
+    qreal revolutionsPersSecond();
+    int numberOfLines();
+    int lineLength();
+    int lineWidth();
+    int innerRadius();
+
+    bool isSpinning() const;
+
+private slots:
+    void rotate();
+
+protected:
+    void paintEvent(QPaintEvent *paintEvent);
+
+private:
+    static int lineCountDistanceFromPrimary(int current, int primary,
+                                            int totalNrOfLines);
+    static QColor currentLineColor(int distance, int totalNrOfLines,
+                                   qreal trailFadePerc, qreal minOpacity,
+                                   QColor color);
+
+    void initialize();
+    void updateSize();
+    void updateTimer();
+    void updatePosition();
+
+private:
+    QColor  _color;
+    qreal   _roundness; // 0..100
+    qreal   _minimumTrailOpacity;
+    qreal   _trailFadePercentage;
+    qreal   _revolutionsPerSecond;
+    int     _numberOfLines;
+    int     _lineLength;
+    int     _lineWidth;
+    int     _innerRadius;
+
+private:
+    WaitingSpinnerWidget(const WaitingSpinnerWidget&);
+    WaitingSpinnerWidget& operator=(const WaitingSpinnerWidget&);
+
+    QTimer *_timer;
+    bool    _centerOnParent;
+    bool    _disableParentWhenSpinning;
+    int     _currentCounter;
+    bool    _isSpinning;
+};
index c9cede58d97c970c3834b5abf216ef1e5e6b223a..95bb2550976e688edbc00049029a91a75bf870a9 100644 (file)
@@ -18,8 +18,8 @@
 #define STRINGIFY(x) STR(x)
 
 YT3 &YT3::instance() {
-    static YT3 *i = new YT3();
-    return *i;
+    static YT3 i;
+    return i;
 }
 
 const QString &YT3::baseUrl() {
@@ -28,7 +28,6 @@ const QString &YT3::baseUrl() {
 }
 
 YT3::YT3() {
-    initApiKeys();
 }
 
 void YT3::initApiKeys() {
index bfc2d42e101105b311b7cff6f09722b727bf527e..550bdb665c9baadcebd369a9724a6c0cbc000491 100644 (file)
@@ -28,6 +28,9 @@ $END_LICENSE */
 
 #include "iconutils.h"
 
+#include "videoapi.h"
+#include "ivchannel.h"
+
 YTChannel::YTChannel(const QString &channelId, QObject *parent)
     : QObject(parent), id(0), channelId(channelId), loadingThumbnail(false), notifyCount(0),
       checked(0), watched(0), loaded(0), loading(false) {}
@@ -80,15 +83,29 @@ void YTChannel::maybeLoadfromAPI() {
 
     loading = true;
 
-    QUrl url = YT3::instance().method("channels");
-    QUrlQuery q(url);
-    q.addQueryItem("id", channelId);
-    q.addQueryItem("part", "snippet");
-    url.setQuery(q);
-
-    QObject *reply = HttpUtils::yt().get(url);
-    connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResponse(QByteArray)));
-    connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
+    if (VideoAPI::impl() == VideoAPI::YT3) {
+        QUrl url = YT3::instance().method("channels");
+        QUrlQuery q(url);
+        q.addQueryItem("id", channelId);
+        q.addQueryItem("part", "snippet");
+        url.setQuery(q);
+
+        QObject *reply = HttpUtils::yt().get(url);
+        connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResponse(QByteArray)));
+        connect(reply, SIGNAL(error(QString)), SLOT(requestError(QString)));
+    } else if (VideoAPI::impl() == VideoAPI::IV) {
+        auto ivChannel = new IVChannel(channelId);
+        connect(ivChannel, &IVChannel::error, this, &YTChannel::requestError);
+        connect(ivChannel, &IVChannel::loaded, this, [this, ivChannel] {
+            displayName = ivChannel->getDisplayName();
+            description = ivChannel->getDescription();
+            thumbnailUrl = ivChannel->getThumbnailUrl();
+            ivChannel->deleteLater();
+            emit infoLoaded();
+            storeInfo();
+            loading = false;
+        });
+    }
 }
 
 void YTChannel::parseResponse(const QByteArray &bytes) {
index f513859ba701ebad3216e67c27c545aab6f0c34b..f59610ca7744f838e3304d63d4979379e394d637 100644 (file)
@@ -36,6 +36,7 @@ public:
     void abort();
     QString getName();
     const QList<QAction *> &getActions();
+    int maxResults() { return 50; }
     SearchParams *getSearchParams() const { return searchParams; }
     static QString videoIdFromUrl(const QString &url);
     static QTime videoTimestampFromUrl(const QString &url);
index 123548148e56320d35c2affde128f276e4e91250..b1870f387f116cb3af473f333cc5b5c4d76af0ba 100644 (file)
@@ -279,7 +279,7 @@ void YTVideo::loadWebPage() {
 void YTVideo::loadEmbedPage() {
     QUrl url("https://www.youtube.com/embed/" + videoId);
     auto reply = HttpUtils::yt().get(url);
-    connect(reply, &HttpReply::finished, this, [this](const HttpReply &reply) {
+    connect(reply, &HttpReply::finished, this, [this](auto &reply) {
         if (!reply.isSuccessful()) {
             getVideoInfo();
             return;