--- /dev/null
+BasedOnStyle: LLVM
+IndentWidth: 4
+AccessModifierOffset: -4
+ColumnLimit: 100
+AllowShortIfStatementsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: Inline
+KeepEmptyLinesAtTheStartOfBlocks: false
+ContinuationIndentWidth: 8
+AlignAfterOpenBracket: true
+BinPackParameters: false
+AllowAllParametersOfDeclarationOnNextLine: false
-
+.idea
+cmake-build-debug
*.user
+.DS_Store
+build
--- /dev/null
+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)
--- /dev/null
+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.
# 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;
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.
INCLUDEPATH += $$PWD/src
DEPENDPATH += $$PWD/src
+DEFINES += HTTP
HEADERS += \
$$PWD/src/cachedhttp.h \
--- /dev/null
+{
+ "$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
#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()));
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);
+}
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 {
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;
}
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;
}
HttpReply *head(const QUrl &url);
HttpReply *post(const QUrl &url, const QMap<QString, QString> ¶ms);
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;
--- /dev/null
+ 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>.
# 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
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) {
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) {
--- /dev/null
+BasedOnStyle: LLVM
+IndentWidth: 4
+AccessModifierOffset: -4
+ColumnLimit: 100
+AllowShortIfStatementsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: Inline
+KeepEmptyLinesAtTheStartOfBlocks: false
+ContinuationIndentWidth: 8
+AlignAfterOpenBracket: true
+BinPackParameters: false
+AllowAllParametersOfDeclarationOnNextLine: false
--- /dev/null
+
+.DS_Store
+.vscode
--- /dev/null
+ 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>.
--- /dev/null
+# 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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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();
+ }
+}
--- /dev/null
+#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
--- /dev/null
+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
+}
--- /dev/null
+TEMPLATE = lib
+include(updater.pri)
<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>
<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'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>
<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>
<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>
<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'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>
<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'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>
<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>
<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>
<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>
<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>
<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>
</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>
<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>&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 &Menu Bar</source>
- <translation type="unfinished"/>
+ <translation>Ε&ναλλαγή γραμμής μενού</translation>
</message>
<message>
<source>Menu</source>
- <translation type="unfinished"/>
+ <translation>Μενού</translation>
</message>
<message>
<source>&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>
<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>
<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>
<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>
<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>
<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>
</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>
</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>
<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'absence de licence, l'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'ai investi dans la création de %1.</translation>
<source>Subscribed to %1</source>
<translation>S'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>
<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>
<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>
<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>
<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>
<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>
<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'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>
<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>
<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>
<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>
<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>
<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>
<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'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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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'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>
<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>
<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>
<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>
</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 &Menu Bar</source>
- <translation type="unfinished"/>
+ <translation>切换菜单栏(&M)</translation>
</message>
<message>
<source>Menu</source>
- <translation type="unfinished"/>
+ <translation>菜单</translation>
</message>
<message>
<source>&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>&Forward</source>
- <translation type="unfinished"/>
+ <translation>前进(&F)</translation>
</message>
<message>
<source>Forward to %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>
<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>
-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
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
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 \
src/view.h \
src/playlistmodel.h \
src/videosource.h \
+ src/waitingspinnerwidget.h \
src/ytsearch.h \
src/ytstandardfeed.h \
src/standardfeedsview.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 \
src/playlistitemdelegate.cpp \
src/playlistmodel.cpp \
src/videosource.cpp \
+ src/waitingspinnerwidget.cpp \
src/ytsearch.cpp \
src/ytstandardfeed.cpp \
src/standardfeedsview.cpp \
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)
#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;
QBoxLayout *layout = new QVBoxLayout();
layout->setAlignment(Qt::AlignCenter);
layout->setSpacing(padding);
+ layout->setMargin(padding / 2);
aboutlayout->addLayout(layout);
QColor lightTextColor = palette().text().color();
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);
void AboutView::appear() {
closeButton->setFocus();
+#ifdef UPDATER
+ Updater::instance().checkWithoutUI();
+#endif
}
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/");
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)));
#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);
void ChannelAggregator::start() {
stopped = false;
updateUnwatchedCount();
- QTimer::singleShot(0, this, SLOT(run()));
+ QTimer::singleShot(10000, this, SLOT(run()));
if (!timer->isActive()) timer->start();
}
QString latestVideoId = currentChannel->latestVideoId();
qDebug() << "Comparing" << videoId << latestVideoId;
hasNewVideos = videoId != latestVideoId;
+ } else {
+ qDebug() << "Cannot capture latest video id";
}
if (hasNewVideos) {
if (currentChannel) {
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();
}
#endif
#include "channellistview.h"
+#include "videoapi.h"
+#include "ivchannelsource.h"
+
namespace {
const QString sortByKey = "subscriptionsSortBy";
const QString showUpdatedKey = "subscriptionsShowUpdated";
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();
#include "constants.h"
#include "http.h"
#include "localcache.h"
-#include "throttledhttp.h"
Http &HttpUtils::notCached() {
static Http *h = [] {
http->addRequestHeader("User-Agent", stealthUserAgent());
CachedHttp *cachedHttp = new CachedHttp(*http, "yt");
- cachedHttp->setMaxSeconds(3600);
+ cachedHttp->setMaxSeconds(86400);
return cachedHttp;
}();
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;
}
--- /dev/null
+#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;
+}
--- /dev/null
+#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
--- /dev/null
+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
+
--- /dev/null
+#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);
+ });
+}
--- /dev/null
+#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
--- /dev/null
+#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;
+}
--- /dev/null
+#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
--- /dev/null
+#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);
+}
--- /dev/null
+#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
--- /dev/null
+#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;
+}
--- /dev/null
+#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
--- /dev/null
+#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);
+}
--- /dev/null
+#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
--- /dev/null
+#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;
+}
--- /dev/null
+#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
#include "constants.h"
#include "iconutils.h"
+#include "updateutils.h"
+
#include "mainwindow.h"
#include "searchparams.h"
#include <qtsingleapplication.h>
IconUtils::setSizes({16, 24, 32, 88});
+ UpdateUtils::init();
+
showWindow(app, pkgDataDir);
return app.exec();
#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"
#include "yt3.h"
#include "ytregions.h"
+#include "invidious.h"
+#include "videoapi.h"
+
#ifdef MEDIA_QTAV
#include "mediaqtav.h"
#endif
#include "mediampv.h"
#endif
+#ifdef UPDATER
+#include "updater.h"
+#endif
+
namespace {
MainWindow *mainWindowInstance;
}
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);
}
ChannelAggregator::instance()->start();
- checkForUpdate();
+#ifdef UPDATER
+ Updater::instance().checkWithoutUI();
+#endif
initialized = true;
}
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);
#endif
helpMenu->addAction(getAction("reportIssue"));
helpMenu->addAction(aboutAct);
+#ifdef UPDATER
+ helpMenu->addAction(Updater::instance().getAction());
+#endif
#ifdef APP_MAC_STORE
helpMenu->addSeparator();
}
}
-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();
}
private slots:
void lazyInit();
- void checkForUpdate();
void donate();
void reportIssue();
void about();
void showView(View *view, bool transition = false);
static QString formatTime(qint64 duration);
bool confirmQuit();
- void simpleUpdateDialog(const QString &version);
bool needStatusBar();
void adjustMessageLabelPosition();
#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;
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;
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) {
// 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) {
}
}
+ 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);
}
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);
}
layout->setSpacing(16);
msgLabel = new QLabel();
- msgLabel->setOpenExternalLinks(true);
+ connect(msgLabel, &QLabel::linkActivated, this, &MessageBar::linkActivated);
layout->addWidget(msgLabel);
QToolButton *closeToolButton = new QToolButton();
msgLabel->setText(message);
}
+void MessageBar::setOpenExternalLinks(bool value) {
+ msgLabel->setOpenExternalLinks(value);
+}
+
void MessageBar::paintEvent(QPaintEvent *e) {
Q_UNUSED(e);
QStyleOption o;
public:
MessageBar(QWidget *parent = 0);
void setMessage(const QString &message);
+ void setOpenExternalLinks(bool value);
signals:
+ void linkActivated(const QString &link);
void closed();
protected:
#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
},
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() {
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));
}
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) {
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();
private:
void handleFirstVideo(Video *video);
- void searchMore(int max);
VideoSource *videoSource;
bool searching;
#include "messagebar.h"
#include "painterutils.h"
+#ifdef UPDATER
+#include "updater.h"
+#endif
+
namespace {
const QString recentKeywordsKey = "recentKeywords";
const QString recentChannelsKey = "recentChannels";
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);
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);
}
}
}
+
+#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
}
#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);
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));
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);
}
+++ /dev/null
-/* $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;
-}
+++ /dev/null
-/* $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
--- /dev/null
+#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
--- /dev/null
+#ifndef UPDATEUTILS_H
+#define UPDATEUTILS_H
+
+namespace UpdateUtils {
+
+void init();
+
+};
+
+#endif // UPDATEUTILS_H
--- /dev/null
+#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
static const QList<QAction *> noActions;
return noActions;
}
+ virtual int maxResults() { return 0; }
public slots:
void setParam(const QString &name, const QVariant &value);
void VideoSourceWidget::previewVideo(const QVector<Video *> &videos) {
videoSource->disconnect();
if (videos.isEmpty()) {
+ qDebug() << "Unavailable video source" << videoSource->getName();
emit unavailable(this);
return;
}
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)));
--- /dev/null
+/* 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;
+}
--- /dev/null
+/* 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;
+};
#define STRINGIFY(x) STR(x)
YT3 &YT3::instance() {
- static YT3 *i = new YT3();
- return *i;
+ static YT3 i;
+ return i;
}
const QString &YT3::baseUrl() {
}
YT3::YT3() {
- initApiKeys();
}
void YT3::initApiKeys() {
#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) {}
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) {
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);
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;