mirror of
https://github.com/QuasarApp/CQtDeployer.git
synced 2025-04-27 18:24:33 +00:00
added support custom templates for qif
This commit is contained in:
parent
4dd65ed872
commit
1f1c05d6b6
@ -19,7 +19,7 @@ TEMPLATE = lib
|
||||
|
||||
DEFINES += DEPLOY_LIBRARY
|
||||
|
||||
VERSION = 1.5.0.0
|
||||
VERSION = 1.5.0.1
|
||||
|
||||
DEFINES += APP_VERSION='\\"$$VERSION\\"'
|
||||
|
||||
|
@ -80,7 +80,7 @@ bool iDistribution::unpackDir(const QString &resource,
|
||||
|
||||
|
||||
QDir res(resource);
|
||||
auto list = res.entryInfoList();
|
||||
auto list = res.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries);
|
||||
|
||||
for (const auto & item :list) {
|
||||
|
||||
|
@ -72,130 +72,60 @@ bool QIF::deployTemplate(PackageControl &pkg) {
|
||||
"js", "qs", "xml"
|
||||
};
|
||||
|
||||
if (customTemplate.isEmpty()) {
|
||||
// default template
|
||||
QString defaultPackageTempalte = ":/Templates/QIF/Distributions/Templates/qif/packages/default";
|
||||
QString defaultConfigCustomDesigne = ":/Templates/QIF/Distributions/Templates/qif/config custom designe/";
|
||||
QString defaultConfig = ":/Templates/QIF/Distributions/Templates/qif/config/";
|
||||
QHash<QString, QString> pakcagesTemplates;
|
||||
|
||||
for (auto it = cfg->packages().begin();
|
||||
it != cfg->packages().end(); ++it) {
|
||||
auto package = it.value();
|
||||
if (!customTemplate.isEmpty()) {
|
||||
QuasarAppUtils::Params::log("Using custom template for installer: " + customTemplate,
|
||||
QuasarAppUtils::Info);
|
||||
|
||||
auto availablePacakages = QDir(defaultPackageTempalte).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
TemplateInfo info;
|
||||
info.Name = PathUtils::stripPath(it.key());
|
||||
bool fDefaultPakcage = cfg->getDefaultPackage() == info.Name;
|
||||
|
||||
if (fDefaultPakcage) {
|
||||
QFileInfo targetInfo(*package.targets().begin());
|
||||
info.Name = targetInfo.baseName();
|
||||
}
|
||||
|
||||
if (!package.name().isEmpty()) {
|
||||
info.Name = package.name();
|
||||
}
|
||||
|
||||
auto location = cfg->getTargetDir() + "/" + getLocation() + "/packages/" + info.Name;
|
||||
auto locationData = location + "/data";
|
||||
if (cfg->getDefaultPackage() != info.Name) {
|
||||
locationData += "/" + info.Name;
|
||||
}
|
||||
|
||||
info.Description = "This package contains the " + info.Name;
|
||||
if (!package.description().isEmpty())
|
||||
info.Description = package.description();
|
||||
|
||||
info.Version = "1.0";
|
||||
if (!package.version().isEmpty())
|
||||
info.Version = package.version();
|
||||
|
||||
info.ReleaseData = QDate::currentDate().toString("yyyy-MM-dd");
|
||||
if (!package.releaseData().isEmpty())
|
||||
info.ReleaseData = package.releaseData();
|
||||
|
||||
info.Icon = "icons/Icon.png";
|
||||
if (package.icon().isEmpty()) {
|
||||
if (!copyFile(":/Templates/QIF/Distributions/Templates/qif/Icon.png",
|
||||
locationData + "/icons/", false)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
QFileInfo iconInfo(package.icon());
|
||||
info.Icon = info.Name + "/icons/" + iconInfo.fileName();
|
||||
if (!copyFile(package.icon(), locationData + "/icons/", false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
info.Publisher = "Company";
|
||||
if (!package.publisher().isEmpty())
|
||||
info.Publisher = package.publisher();
|
||||
|
||||
QString cmdArray = "[";
|
||||
int initSize = cmdArray.size();
|
||||
for (const auto &target :package.targets()) {
|
||||
auto fileinfo = QFileInfo(target);
|
||||
if (fileinfo.suffix().compare("exe", ONLY_WIN_CASE_INSENSIATIVE) == 0 || fileinfo.suffix().isEmpty()) {
|
||||
if (cmdArray.size() > initSize) {
|
||||
cmdArray += ",";
|
||||
}
|
||||
cmdArray += "\"" + info.Name + "/" + fileinfo.fileName() + "\"";
|
||||
}
|
||||
}
|
||||
cmdArray += "]";
|
||||
|
||||
info.Custom = {{"[\"array\", \"of\", \"cmds\"]", cmdArray},
|
||||
{"$LOCAL_ICON", info.Name + "/icons/" + QFileInfo(info.Icon).fileName()}};
|
||||
|
||||
|
||||
if (info.Name.isEmpty()) {
|
||||
info.Name = "Application";
|
||||
}
|
||||
|
||||
if (!unpackDir(":/Templates/QIF/Distributions/Templates/qif/packages/default",
|
||||
location, info, sufixes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pkg.movePackage(it.key(), locationData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fDefaultPakcage)
|
||||
generalInfo = info;
|
||||
|
||||
for (const auto& pkg: availablePacakages) {
|
||||
pakcagesTemplates.insert(pkg.fileName(), pkg.absoluteFilePath());
|
||||
}
|
||||
|
||||
auto configLocation = cfg->getTargetDir() + "/" + getLocation() + "/config/";
|
||||
|
||||
auto qifStyle = getStyle(QuasarAppUtils::Params::getStrArg("qifStyle", ""));
|
||||
auto qifBanner = QuasarAppUtils::Params::getStrArg("qifBanner", "");
|
||||
auto qifLogo = QuasarAppUtils::Params::getStrArg("qifLogo", "");
|
||||
|
||||
auto configTemplate = ":/Templates/QIF/Distributions/Templates/qif/config/";
|
||||
if (qifStyle.size() || qifBanner.size() || qifLogo.size()) {
|
||||
configTemplate = ":/Templates/QIF/Distributions/Templates/qif/config custom designe/";
|
||||
}
|
||||
|
||||
if (!unpackDir(configTemplate,
|
||||
configLocation, generalInfo, sufixes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qifStyle.size() && !copyFile(qifStyle, configLocation + "/style.css", true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qifBanner.size() && !copyFile(qifBanner, configLocation + "/banner.png", true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qifLogo.size() && !copyFile(qifLogo, configLocation + "/logo.png", true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
defaultConfigCustomDesigne = customTemplate + "/config";
|
||||
defaultConfig = customTemplate + "/config";
|
||||
}
|
||||
|
||||
// custom template
|
||||
for (auto it = cfg->packages().begin();
|
||||
it != cfg->packages().end(); ++it) {
|
||||
|
||||
if (!deployPackage(it, cfg, sufixes, pakcagesTemplates, defaultPackageTempalte, pkg)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto configLocation = cfg->getTargetDir() + "/" + getLocation() + "/config/";
|
||||
|
||||
auto qifStyle = getStyle(QuasarAppUtils::Params::getStrArg("qifStyle", ""));
|
||||
auto qifBanner = QuasarAppUtils::Params::getStrArg("qifBanner", "");
|
||||
auto qifLogo = QuasarAppUtils::Params::getStrArg("qifLogo", "");
|
||||
|
||||
auto configTemplate = defaultConfig;
|
||||
if (qifStyle.size() || qifBanner.size() || qifLogo.size()) {
|
||||
configTemplate = defaultConfigCustomDesigne;
|
||||
}
|
||||
|
||||
if (!unpackDir(configTemplate,
|
||||
configLocation, generalInfo, sufixes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qifStyle.size() && !copyFile(qifStyle, configLocation + "/style.css", true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qifBanner.size() && !copyFile(qifBanner, configLocation + "/banner.png", true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qifLogo.size() && !copyFile(qifLogo, configLocation + "/logo.png", true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -217,11 +147,8 @@ bool QIF::removeTemplate() const {
|
||||
const DeployConfig *cfg = DeployCore::_config;
|
||||
|
||||
registerOutFiles();
|
||||
if (customTemplate.isEmpty()) {
|
||||
return QDir(cfg->getTargetDir() + "/" + getLocation()).removeRecursively();
|
||||
}
|
||||
return QDir(cfg->getTargetDir() + "/" + getLocation()).removeRecursively();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QProcessEnvironment QIF::processEnvirement() const {
|
||||
@ -263,3 +190,96 @@ QString QIF::installerFile() const {
|
||||
return DeployCore::_config->getTargetDir() + "/Installer" + generalInfo.Name + sufix;
|
||||
}
|
||||
|
||||
bool QIF::deployPackage(const QHash<QString, DistroModule>::const_iterator& it,
|
||||
const DeployConfig * cfg,
|
||||
const QStringList sufixes,
|
||||
const QHash<QString, QString>& pakcagesTemplates,
|
||||
const QString& defaultPackageTempalte,
|
||||
PackageControl &pkg) {
|
||||
auto package = it.value();
|
||||
|
||||
TemplateInfo info;
|
||||
info.Name = PathUtils::stripPath(it.key());
|
||||
bool fDefaultPakcage = cfg->getDefaultPackage() == info.Name;
|
||||
|
||||
if (fDefaultPakcage) {
|
||||
QFileInfo targetInfo(*package.targets().begin());
|
||||
info.Name = targetInfo.baseName();
|
||||
}
|
||||
|
||||
if (!package.name().isEmpty()) {
|
||||
info.Name = package.name();
|
||||
}
|
||||
|
||||
auto location = cfg->getTargetDir() + "/" + getLocation() + "/packages/" + info.Name;
|
||||
auto locationData = location + "/data";
|
||||
if (cfg->getDefaultPackage() != info.Name) {
|
||||
locationData += "/" + info.Name;
|
||||
}
|
||||
|
||||
info.Description = "This package contains the " + info.Name;
|
||||
if (!package.description().isEmpty())
|
||||
info.Description = package.description();
|
||||
|
||||
info.Version = "1.0";
|
||||
if (!package.version().isEmpty())
|
||||
info.Version = package.version();
|
||||
|
||||
info.ReleaseData = QDate::currentDate().toString("yyyy-MM-dd");
|
||||
if (!package.releaseData().isEmpty())
|
||||
info.ReleaseData = package.releaseData();
|
||||
|
||||
info.Icon = "icons/Icon.png";
|
||||
if (package.icon().isEmpty()) {
|
||||
if (!copyFile(":/Templates/QIF/Distributions/Templates/qif/Icon.png",
|
||||
locationData + "/icons/", false)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
QFileInfo iconInfo(package.icon());
|
||||
info.Icon = info.Name + "/icons/" + iconInfo.fileName();
|
||||
if (!copyFile(package.icon(), locationData + "/icons/", false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
info.Publisher = "Company";
|
||||
if (!package.publisher().isEmpty())
|
||||
info.Publisher = package.publisher();
|
||||
|
||||
QString cmdArray = "[";
|
||||
int initSize = cmdArray.size();
|
||||
for (const auto &target :package.targets()) {
|
||||
auto fileinfo = QFileInfo(target);
|
||||
if (fileinfo.suffix().compare("exe", ONLY_WIN_CASE_INSENSIATIVE) == 0 || fileinfo.suffix().isEmpty()) {
|
||||
if (cmdArray.size() > initSize) {
|
||||
cmdArray += ",";
|
||||
}
|
||||
cmdArray += "\"" + info.Name + "/" + fileinfo.fileName() + "\"";
|
||||
}
|
||||
}
|
||||
cmdArray += "]";
|
||||
|
||||
info.Custom = {{"[\"array\", \"of\", \"cmds\"]", cmdArray},
|
||||
{"$LOCAL_ICON", info.Name + "/icons/" + QFileInfo(info.Icon).fileName()}};
|
||||
|
||||
|
||||
if (info.Name.isEmpty()) {
|
||||
info.Name = "Application";
|
||||
}
|
||||
|
||||
if (!unpackDir(pakcagesTemplates.value(package.name(), defaultPackageTempalte),
|
||||
location, info, sufixes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pkg.movePackage(it.key(), locationData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fDefaultPakcage)
|
||||
generalInfo = info;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
#include "idistribution.h"
|
||||
|
||||
class PackageControl;
|
||||
class DeployConfig;
|
||||
|
||||
/**
|
||||
* @brief The QIF class provides interface betvin deployment targets and Qt Installer Framework
|
||||
*/
|
||||
@ -20,12 +22,29 @@ public:
|
||||
QStringList outPutFiles() const override;
|
||||
|
||||
private:
|
||||
QString binarycreator;
|
||||
TemplateInfo generalInfo;
|
||||
|
||||
QString getStyle(const QString &input) const;
|
||||
QString installerFile() const;
|
||||
|
||||
/**
|
||||
* @brief deployPackage - private method for deploy package of qt installer framework
|
||||
* @param it - this is const iterator of current DistroModule.
|
||||
* @param cfg - this is config pointer
|
||||
* @param sufixes - this is sufixses of files for copy into package
|
||||
* @param pakcagesTemplates - this is list of pakcages and them tempalte patheses
|
||||
* @param defaultPackageTempalte this is path to default package template
|
||||
* @param pkg this is PackageControl object for move a packge data.
|
||||
* @return return true if package deployed successful
|
||||
*/
|
||||
bool deployPackage(const QHash<QString, DistroModule>::const_iterator &it,
|
||||
const DeployConfig *cfg,
|
||||
const QStringList sufixes,
|
||||
const QHash<QString, QString> &pakcagesTemplates,
|
||||
const QString &defaultPackageTempalte,
|
||||
PackageControl &pkg);
|
||||
|
||||
QString binarycreator;
|
||||
TemplateInfo generalInfo;
|
||||
};
|
||||
|
||||
#endif // QIF_H
|
||||
|
@ -189,7 +189,8 @@ void DeployCore::help() {
|
||||
" this option has been disabled by default, as it can add low-level graphics libraries to the distribution,"
|
||||
" which will not be compatible with equipment on users' hosts."},
|
||||
{"allQmlDependes", "Extracts all the qml libraries. (not recommended, as it takes great amount of computer memory)"},
|
||||
{"qif", "Create the QIF installer for deployement programm"},
|
||||
{"qif", "Create the QIF installer for deployement programm"
|
||||
" You can specify the path to your own installer template. Examples: cqtdeployer -qif path/to/myCustom/qif."},
|
||||
{"qifFromSystem", "force use system binarycreator tool of qif from path or qt"},
|
||||
{"zip", "Create the ZIP arhive for deployement programm"},
|
||||
{"deploySystem", "Deploys all libraries (on snap version you need to turn on permission)"},
|
||||
|
@ -3,7 +3,7 @@
|
||||
<WizardDefaultWidth>640px</WizardDefaultWidth>
|
||||
<WizardDefaultHeight>400px</WizardDefaultHeight>
|
||||
<Name>CQtDeployer</Name>
|
||||
<Version>1.5.0.0</Version>
|
||||
<Version>1.5.0.1</Version>
|
||||
<Title>CQtDeployer</Title>
|
||||
<Publisher>QuasarApp</Publisher>
|
||||
<StartMenuDir>CQtDeployer</StartMenuDir>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<WizardDefaultWidth>640px</WizardDefaultWidth>
|
||||
<WizardDefaultHeight>400px</WizardDefaultHeight>
|
||||
<Name>CQtDeployer</Name>
|
||||
<Version>1.5.0.0</Version>
|
||||
<Version>1.5.0.1</Version>
|
||||
<Title>CQtDeployer</Title>
|
||||
<Publisher>QuasarApp</Publisher>
|
||||
<StartMenuDir>CQtDeployer</StartMenuDir>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Package>
|
||||
<DisplayName>CQtDeployer 1.5 Alpha</DisplayName>
|
||||
<Description>CQtDeployer 1.5 Alpha. Do not use this version because it is unstable and may lead to unwanted bugs or consequences. Use this version exclusively for testing new functionality.</Description>
|
||||
<Version>1.5.0.0</Version>
|
||||
<Version>1.5.0.1</Version>
|
||||
<Default>true</Default>
|
||||
<ForcedInstallation>false</ForcedInstallation>
|
||||
<Script>installscript.js</Script>
|
||||
|
12
UnitTests/testRes/QIFCustomTemplate/config/config.xml
Normal file
12
UnitTests/testRes/QIFCustomTemplate/config/config.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Installer>
|
||||
<Name>Stylesheet Example</Name>
|
||||
<Version>1.0.0</Version>
|
||||
<Title>Stylesheet Example</Title>
|
||||
<Publisher>Qt-Project</Publisher>
|
||||
<StartMenuDir>Qt IFW Examples</StartMenuDir>
|
||||
<TargetDir>@HomeDir@/IfwExamples/stylesheet</TargetDir>
|
||||
<WizardStyle>Classic</WizardStyle>
|
||||
<StyleSheet>style.qss</StyleSheet>
|
||||
<TitleColor>#FFFFFF</TitleColor>
|
||||
</Installer>
|
25
UnitTests/testRes/QIFCustomTemplate/config/style.qss
Normal file
25
UnitTests/testRes/QIFCustomTemplate/config/style.qss
Normal file
@ -0,0 +1,25 @@
|
||||
QWidget
|
||||
{
|
||||
color: white;
|
||||
background-color: rgb(65, 65, 65);
|
||||
}
|
||||
|
||||
QPushButton
|
||||
{
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(150, 150, 150, 60%), stop:1 rgba(50, 50, 50, 60%));
|
||||
border-color: rgb(60, 60, 60);
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-radius: 9px;
|
||||
min-height: 20px;
|
||||
max-height: 20px;
|
||||
min-width: 60px;
|
||||
max-width: 60px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
QPushButton:pressed, QPushButton:checked
|
||||
{
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(50, 50, 50, 60%), stop:1 rgba(150, 150, 150, 60%));
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/**************************************************************************
|
||||
**
|
||||
** Copyright (C) 2015 The Qt Company Ltd.
|
||||
** Contact: http://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the Qt Installer Framework.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see http://qt.io/terms-conditions. For further
|
||||
** information use the contact form at http://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 2.1 or version 3 as published by the Free
|
||||
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
|
||||
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
|
||||
** following information to ensure the GNU Lesser General Public License
|
||||
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
|
||||
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
|
||||
**
|
||||
** As a special exception, The Qt Company gives you certain additional
|
||||
** rights. These rights are described in The Qt Company LGPL Exception
|
||||
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
|
||||
**
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
**************************************************************************/
|
||||
|
||||
function Component()
|
||||
{
|
||||
// constructor
|
||||
installer.setDefaultPageVisible(QInstaller.ComponentSelection, false);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Package>
|
||||
<DisplayName>Dummy</DisplayName>
|
||||
<ReleaseDate>2015-09-09</ReleaseDate>
|
||||
<Version>1.0.1</Version>
|
||||
<Default>true</Default>
|
||||
<Script>installscript.qs</Script>
|
||||
</Package>
|
@ -633,7 +633,8 @@ void deploytest::testQIF() {
|
||||
runTestParams({"-bin", bin, "clear" ,
|
||||
"-qmake", qmake,
|
||||
"-qmlDir", TestBinDir + "/../TestQMLWidgets",
|
||||
"qif", "qifFromSystem", "verbose"}, &comapareTree, {}, true);
|
||||
"-qif", TestBinDir + "/../../UnitTests/testRes/QIFCustomTemplate",
|
||||
"qifFromSystem", "verbose"}, &comapareTree, {}, true);
|
||||
|
||||
// test clear for qif
|
||||
runTestParams({"clear", "verbose"}, {} , {}, true);
|
||||
|
2
doc/wiki
2
doc/wiki
@ -1 +1 @@
|
||||
Subproject commit d9d5576252429c6ef0c1acccb0ed57f1bce1d7d4
|
||||
Subproject commit 684a330f92ec9790a5a485eeb93732971d54a636
|
@ -1,5 +1,5 @@
|
||||
[Desktop Entry]
|
||||
Version=1.5.0.0
|
||||
Version=1.5.0.1
|
||||
Name=CQtDeployer
|
||||
Comment=CQtDeployer Help.
|
||||
Exec=cqtdeployer
|
||||
@ -10,6 +10,6 @@ Categories=Application;
|
||||
X-GNOME-Bugzilla-Bugzilla=GNOME
|
||||
X-GNOME-Bugzilla-Product=CQtDeployer
|
||||
X-GNOME-Bugzilla-Component=General
|
||||
X-GNOME-Bugzilla-Version=1.5.0.0
|
||||
X-GNOME-Bugzilla-Version=1.5.0.1
|
||||
StartupNotify=true
|
||||
Name[ru_RU]=CQtDeployer
|
||||
|
@ -6,7 +6,7 @@
|
||||
#
|
||||
|
||||
name: cqtdeployer # you probably want to 'snapcraft register <name>'
|
||||
version: '1.5.0.0' # just for humans, typically '1.2+git' or '1.3.2'
|
||||
version: '1.5.0.1' # just for humans, typically '1.2+git' or '1.3.2'
|
||||
summary: deploy your qt projects # 79 char long summary
|
||||
description: |
|
||||
Console app for deploy qt libs.
|
||||
|
Loading…
x
Reference in New Issue
Block a user