mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-24 19:25:12 +03:00
feat(repo): add setup scripts (#1189)
This commit is contained in:
parent
5c5d8f811f
commit
df32e18be3
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@ -53,7 +53,9 @@ Hi! We, the maintainers, are really excited that you are interested in contribut
|
||||
|
||||
First, [join our Discord server](https://discord.gg/SpmNs4S) and let us know that you want to contribute. This way we can point you in the right direction and help ensure your contribution will be as helpful as possible. We also recommend you read the [technical details page](https://tauri.studio/en/docs/getting-started/technical-details) to learn how Tauri works under the hood and familiarize yourself with the codebase.
|
||||
|
||||
To set up your machine for development, follow the [Tauri setup guide](https://tauri.studio/en/docs/getting-started/intro#setting-up-your-environment) to get all the tools you need to develop Tauri apps. The only additional tool you may need is [Yarn](https://yarnpkg.com/), it is only required if you are developing the Node CLI/API (`tauri.js`). Next, clone the Tauri repo. It is structured as a monorepo, which means that all the various Tauri packages are under the same repository. The development process varies depending on what part of Tauri you are contributing to.
|
||||
To set up your machine for development, follow the [Tauri setup guide](https://tauri.studio/en/docs/getting-started/intro#setting-up-your-environment) to get all the tools you need to develop Tauri apps. The only additional tool you may need is [Yarn](https://yarnpkg.com/), it is only required if you are developing the Node CLI/API (`cli/tauri.js` and `api`). Next, clone the Tauri repo. It is structured as a monorepo, which means that all the various Tauri packages are under the same repository. The development process varies depending on what part of Tauri you are contributing to.
|
||||
|
||||
To build all Tauri packages (`@tauri-apps/api`, `Core Rust CLI`, `@tauri-apps/cli`), use the `.scripts/setup.sh` (Linux and macOS) or `.scripts/setup.ps1` (Windows) scripts.
|
||||
|
||||
### Developing The CLI and API (`tauri.js`)
|
||||
|
||||
|
6
.github/workflows/core-lint-fmt.yml
vendored
6
.github/workflows/core-lint-fmt.yml
vendored
@ -25,9 +25,6 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
name: workspace
|
||||
env:
|
||||
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
|
||||
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
@ -79,6 +76,3 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --manifest-path ./tauri/Cargo.toml --all-targets --features ${{ matrix.feature }} -- -D warnings
|
||||
name: core
|
||||
env:
|
||||
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
|
||||
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
|
||||
|
6
.github/workflows/test-core.yml
vendored
6
.github/workflows/test-core.yml
vendored
@ -36,15 +36,9 @@ jobs:
|
||||
run: |
|
||||
cd ./tauri
|
||||
cargo build
|
||||
env:
|
||||
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
|
||||
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
|
||||
- name: test
|
||||
run: |
|
||||
cargo test
|
||||
env:
|
||||
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
|
||||
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
|
||||
|
||||
test-tauri-cli:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
@ -15,35 +15,26 @@ function check_error {
|
||||
}
|
||||
}
|
||||
|
||||
# run n+1 times, where n is the amount of mutually exclusive features.
|
||||
# the extra run is for all the crates without mutually exclusive features.
|
||||
# as many features as possible are enabled at for each command
|
||||
function mutex {
|
||||
function run {
|
||||
$command, $_args = $args
|
||||
|
||||
foreach ($feature in @("no-server","embedded-server")) {
|
||||
Write-Output "[$command][$feature] tauri"
|
||||
cargo $command --manifest-path tauri/Cargo.toml --all-targets --features "$feature,cli,all-api" $_args
|
||||
check_error
|
||||
}
|
||||
|
||||
Write-Output "[$command] other crates"
|
||||
cargo $command --workspace --exclude tauri --all-targets --all-features $_args
|
||||
Write-Output "[$command]"
|
||||
cargo $command --workspace --all-targets --all-features $_args
|
||||
check_error
|
||||
}
|
||||
|
||||
foreach ($command in $args) {
|
||||
Switch ($command) {
|
||||
"check" {
|
||||
mutex check
|
||||
run check
|
||||
break
|
||||
}
|
||||
"test" {
|
||||
mutex test
|
||||
run test
|
||||
break
|
||||
}
|
||||
"clippy" {
|
||||
mutex clippy "--" -D warnings
|
||||
run clippy "--" -D warnings
|
||||
break
|
||||
}
|
||||
"fmt" {
|
||||
|
@ -14,26 +14,19 @@ fi
|
||||
# run n+1 times, where n is the amount of mutually exclusive features.
|
||||
# the extra run is for all the crates without mutually exclusive features.
|
||||
# as many features as possible are enabled at for each command
|
||||
mutex() {
|
||||
run() {
|
||||
command=$1
|
||||
shift 1
|
||||
|
||||
for feature in "no-server" "embedded-server"; do
|
||||
echo "[$command][$feature] tauri"
|
||||
cargo "$command" --manifest-path tauri/Cargo.toml --all-targets --features "$feature,cli,all-api" "$@"
|
||||
done
|
||||
|
||||
echo "[$command] other crates"
|
||||
cargo "$command" --workspace --exclude tauri --all-targets --all-features "$@"
|
||||
cargo "$command" --workspace --all-targets --all-features "$@"
|
||||
}
|
||||
|
||||
for command in "$@"; do
|
||||
case "$command" in
|
||||
check | test)
|
||||
mutex "$command"
|
||||
run "$command"
|
||||
;;
|
||||
clippy)
|
||||
mutex clippy -- -D warnings
|
||||
run clippy -- -D warnings
|
||||
;;
|
||||
fmt)
|
||||
echo "[$command] checking formatting"
|
||||
|
@ -1,28 +0,0 @@
|
||||
@echo OFF
|
||||
echo "Setting up enviromental Variables"
|
||||
|
||||
rem check script execution directory vs script directory.
|
||||
|
||||
IF "%cd%\"=="%~dp0" (
|
||||
GOTO exitnodir
|
||||
)
|
||||
|
||||
rem setup relative paths from root folder
|
||||
set "TAURI_DIST_DIR=%~1tauri\examples\communication\dist"
|
||||
set "TAURI_DIR=%~1tauri\examples\communication\src-tauri"
|
||||
rem convert relative path to absolute path and re-set it into the enviroment var
|
||||
for /F "delims=" %%F IN ("%TAURI_DIST_DIR%") DO SET "TAURI_DIST_DIR=%%~fF"
|
||||
for /F "delims=" %%F IN ("%TAURI_DIR%") DO SET "TAURI_DIR=%%~fF"
|
||||
|
||||
if NOT EXIST %TAURI_DIR% GOTO exitnodir
|
||||
if NOT EXIST %TAURI_DIST_DIR% GOTO exitnodir
|
||||
|
||||
GOTO exitfine
|
||||
|
||||
:exitnodir
|
||||
echo "Variables are not setup properly. Please run from Tauri Root directory"
|
||||
@EXIT /B 1
|
||||
|
||||
:exitfine
|
||||
echo "Variables set, ready to work!"
|
||||
@EXIT /B 0
|
@ -1,17 +0,0 @@
|
||||
Write-Output "Setting up enviromental Variables"
|
||||
# setup relative paths
|
||||
$dist_path = "tauri\examples\communication\dist"
|
||||
$src_path = "tauri\examples\communication\src-tauri"
|
||||
|
||||
# check to see if path variables are directories
|
||||
if ((Test-Path $dist_path -PathType Any) -Or (Test-Path $src_path -PathType Any)) {
|
||||
# convert relative paths to absolute paths.
|
||||
# put these absolute paths in enviromental variables
|
||||
$env:TAURI_DIST_DIR = Resolve-Path $dist_path
|
||||
$env:TAURI_DIR = Resolve-Path $src_path
|
||||
Write-Output "Variables set, ready to work!"
|
||||
|
||||
}
|
||||
else {
|
||||
Write-Output "Variables are not setup properly. Please run from Tauri Root directory"
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Note: Script must be run like this `. .init_env.sh` to setup variables for your current shell
|
||||
# define relative paths
|
||||
|
||||
DistPath='tauri/examples/communication/dist'
|
||||
SrcPath='tauri/examples/communication/src-tauri'
|
||||
|
||||
echo "Setting up enviroment Variables"
|
||||
|
||||
# check if relative paths exist
|
||||
if [ -d "${DistPath}" ]||[ -d "${SrcPath}" ]
|
||||
then
|
||||
# Convert to absolute paths
|
||||
DistPath="$(cd "${DistPath}" && pwd -P)"
|
||||
SrcPath="$(cd "${SrcPath}" && pwd -P)"
|
||||
|
||||
# export enviromental variables
|
||||
export TAURI_DIST_DIR=${DistPath}
|
||||
export TAURI_DIR=${SrcPath}
|
||||
echo "Variables set, ready to work!"
|
||||
|
||||
else
|
||||
# if directories don't exist then exit script and tell user run script in root dir.
|
||||
echo "Error: Variables are not setup properly. Please run from Tauri Root directory '. .scripts/init_env.sh'"
|
||||
fi
|
30
.scripts/setup.ps1
Normal file
30
.scripts/setup.ps1
Normal file
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env pwsh
|
||||
echo "Building API definitions..."
|
||||
cd api
|
||||
yarn; yarn build
|
||||
cd ..
|
||||
|
||||
echo "Building the Tauri CLI..."
|
||||
cd cli\core
|
||||
cargo build --release
|
||||
cd ..\..
|
||||
|
||||
Set-Alias rtauri "$(pwd)\cli\core\target\release\cargo-tauri.exe"
|
||||
echo "Added alias 'rtauri' for '$(pwd)\cli\core\target\release\cargo-tauri.exe'"
|
||||
echo "Tauri CLI installed. Run it with '$ rtauri tauri [COMMAND]'."
|
||||
|
||||
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes"
|
||||
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No"
|
||||
$options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
|
||||
|
||||
$result = $host.ui.PromptForChoice("Node.js CLI", "Do you want to use the Node.js CLI?", $options, 1)
|
||||
switch ($result) {
|
||||
0{
|
||||
cd cli\tauri.js
|
||||
yarn; yarn build
|
||||
cd ..\..
|
||||
Set-Alias stauri "$(pwd)\cli\tauri.js\bin\tauri.js"
|
||||
echo "Added alias 'stauri' for '$(pwd)\cli\tauri.js\bin\tauri.js'"
|
||||
echo "Tauri Node.js CLI installed. Run it with '$ stauri [COMMAND]'"
|
||||
}
|
||||
}
|
29
.scripts/setup.sh
Normal file
29
.scripts/setup.sh
Normal file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env sh
|
||||
echo "Building API definitions..."
|
||||
cd api
|
||||
yarn && yarn build
|
||||
cd ..
|
||||
|
||||
echo "Building the Tauri CLI..."
|
||||
cd cli/core
|
||||
cargo build --release
|
||||
cd ../..
|
||||
|
||||
alias rtauri="$(pwd)/cli/core/target/release/cargo-tauri.exe tauri"
|
||||
echo "Added alias 'rtauri' for '$(pwd)/cli/core/target/release/cargo-tauri.exe tauri'"
|
||||
echo "Tauri CLI installed. Run it with '$ rtauri [COMMAND]'."
|
||||
|
||||
echo "Do you want to use the Node.js CLI?"
|
||||
select yn in "Yes" "No"; do
|
||||
case $yn in
|
||||
Yes )
|
||||
cd cli/tauri.js
|
||||
yarn && yarn build
|
||||
cd ../..
|
||||
alias stauri="$(pwd)/cli/tauri.js/bin/tauri.js"
|
||||
echo "Added alias 'stauri' for '$(pwd)/cli/tauri.js/bin/tauri.js'"
|
||||
echo "Tauri Node.js CLI installed. Run it with '$ stauri [COMMAND]'"
|
||||
break;;
|
||||
No ) break;;
|
||||
esac
|
||||
done
|
@ -130,4 +130,4 @@
|
||||
- Create UMD, ESM and CJS artifacts for the JavaScript API entry point from TS source using rollup.
|
||||
- Renaming window.tauri to window.\_\_TAURI\_\_, closing #435.
|
||||
The **Tauri** object now follows the TypeScript API structure (e.g. window.tauri.readTextFile is now window.\_\_TAURI\_\_.fs.readTextFile).
|
||||
If you want to keep the `window.tauri` object for a while, you can add a [mapping object](https://gist.github.com/lucasfernog/8f7b29cadd91d92ee2cf816a20c2ef01) to your code.
|
||||
If you want to keep the `window.tauri` object for a while, you can add a [mapping object](https://gist.github.com/lucasfernog/8f7b29cadd91d92ee2cf816a20c2ef01) to your code.
|
||||
|
@ -21,8 +21,7 @@
|
||||
"lint-fix": "eslint --fix --ext ts \"./src/**/*.ts\"",
|
||||
"lint:lockfile": "lockfile-lint --path yarn.lock --type yarn --validate-https --allowed-hosts npm yarn",
|
||||
"format": "prettier --write --end-of-line=auto !./**/config.validator.js !./**/config.schema.json \"./**/*.{js,jsx,ts,tsx,html,css,json}\" --ignore-path .gitignore",
|
||||
"format:check": "prettier --check --end-of-line=auto !./**/config.validator.js !./**/config.schema.json \"./**/*.{js,jsx,ts,tsx,html,css,json}\" --ignore-path .gitignore",
|
||||
"build:tauri[rust]": "cd ../tauri && TAURI_DIST_DIR=../../test/fixture/dist TAURI_DIR=../test/fixture cargo publish --dry-run --allow-dirty"
|
||||
"format:check": "prettier --check --end-of-line=auto !./**/config.validator.js !./**/config.schema.json \"./**/*.{js,jsx,ts,tsx,html,css,json}\" --ignore-path .gitignore"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,131 +0,0 @@
|
||||
import { existsSync, readFileSync } from 'fs-extra'
|
||||
import { TauriConfig } from 'types'
|
||||
import { merge } from 'webpack-merge'
|
||||
import logger from '../helpers/logger'
|
||||
import * as appPaths from './app-paths'
|
||||
import nonWebpackRequire from '../helpers/non-webpack-require'
|
||||
import chalk from 'chalk'
|
||||
import { isTauriConfig, ajv } from '../types/config.validator'
|
||||
|
||||
const error = logger('ERROR:', chalk.red)
|
||||
|
||||
const getTauriConfig = (cfg: Partial<TauriConfig>): TauriConfig => {
|
||||
const pkgPath = appPaths.resolve.app('package.json')
|
||||
const tauriConfPath = appPaths.resolve.tauri('tauri.conf.json')
|
||||
if (!existsSync(tauriConfPath)) {
|
||||
error(
|
||||
"Could not find a tauri config (tauri.conf.json) in your app's directory."
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
const tauriConf = JSON.parse(
|
||||
readFileSync(tauriConfPath).toString()
|
||||
) as TauriConfig
|
||||
const pkg = existsSync(pkgPath)
|
||||
? (nonWebpackRequire(pkgPath) as { productName: string })
|
||||
: null
|
||||
|
||||
const config = merge(
|
||||
{
|
||||
build: {},
|
||||
ctx: {},
|
||||
tauri: {
|
||||
embeddedServer: {
|
||||
active: true
|
||||
},
|
||||
bundle: {
|
||||
active: true,
|
||||
icon: [],
|
||||
resources: [],
|
||||
externalBin: [],
|
||||
deb: {
|
||||
depends: []
|
||||
},
|
||||
osx: {
|
||||
frameworks: []
|
||||
}
|
||||
},
|
||||
allowlist: {
|
||||
all: false
|
||||
},
|
||||
window: {
|
||||
title: pkg?.productName ?? 'Tauri App'
|
||||
},
|
||||
security: {
|
||||
csp:
|
||||
"default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
|
||||
},
|
||||
inliner: {
|
||||
active: true
|
||||
}
|
||||
}
|
||||
} as any,
|
||||
tauriConf as any,
|
||||
cfg as any
|
||||
) as TauriConfig
|
||||
|
||||
if (!isTauriConfig(config)) {
|
||||
const messages = ajv
|
||||
.errorsText(
|
||||
isTauriConfig.errors
|
||||
?.filter((e) => e.keyword !== 'if')
|
||||
.map((e) => {
|
||||
e.dataPath = e.dataPath.replace(/\./g, ' > ')
|
||||
if (
|
||||
e.keyword === 'additionalProperties' &&
|
||||
typeof e.message === 'string' &&
|
||||
'additionalProperty' in e.params
|
||||
) {
|
||||
e.message = `has unknown property ${e.params.additionalProperty}`
|
||||
}
|
||||
return e
|
||||
}),
|
||||
{ dataVar: 'tauri.conf.json', separator: '\n' }
|
||||
)
|
||||
.split('\n')
|
||||
|
||||
for (const message of messages) {
|
||||
error(message)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const runningDevServer = config.build.devPath?.startsWith('http')
|
||||
if (!runningDevServer) {
|
||||
config.build.devPath = appPaths.resolve.tauri(config.build.devPath)
|
||||
process.env.TAURI_DIST_DIR = config.build.devPath
|
||||
}
|
||||
if (config.build.distDir) {
|
||||
config.build.distDir = appPaths.resolve.tauri(config.build.distDir)
|
||||
process.env.TAURI_DIST_DIR = config.build.distDir
|
||||
}
|
||||
|
||||
// OSX bundle config
|
||||
if (config.tauri.bundle.osx) {
|
||||
const license = config.tauri.bundle.osx.license
|
||||
if (typeof license === 'string') {
|
||||
config.tauri.bundle.osx.license = appPaths.resolve.tauri(license)
|
||||
} else if (license !== null) {
|
||||
const licensePath = appPaths.resolve.app('LICENSE')
|
||||
if (existsSync(licensePath)) {
|
||||
config.tauri.bundle.osx.license = licensePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bundle targets
|
||||
if (Array.isArray(config.tauri.bundle.targets)) {
|
||||
if (process.platform !== 'win32') {
|
||||
config.tauri.bundle.targets = config.tauri.bundle.targets.filter(
|
||||
(t) => t !== 'msi'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
process.env.TAURI_DIR = appPaths.tauriDir
|
||||
process.env.TAURI_CONFIG = JSON.stringify(config)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
export default getTauriConfig
|
@ -11,7 +11,6 @@ module.exports = {
|
||||
'api/tauricon': './src/api/tauricon.ts',
|
||||
'api/info': './src/api/info.ts',
|
||||
'api/dependency-manager': './src/api/dependency-manager/index.ts',
|
||||
'helpers/tauri-config': './src/helpers/tauri-config.ts',
|
||||
'helpers/spawn': './src/helpers/spawn.ts',
|
||||
'helpers/rust-cli': './src/helpers/rust-cli.ts'
|
||||
},
|
||||
|
14
maskfile.md
14
maskfile.md
@ -11,8 +11,7 @@ git clone --recursive git@github.com:tauri-apps/examples.git \
|
||||
cargo build
|
||||
cargo install cargo-web # used by example rust/yew
|
||||
|
||||
cd cli/tauri.js
|
||||
yarn && yarn build
|
||||
. .scripts/setup.sh
|
||||
```
|
||||
|
||||
```powershell
|
||||
@ -39,17 +38,11 @@ if (-Not (Test-Path $CWD\examples -PathType Any)) {
|
||||
# Enter the examples folder and pull the latest data from origin/dev
|
||||
cd examples; git pull origin dev; cd ..
|
||||
|
||||
# set the env vars.
|
||||
$env:TAURI_DIST_DIR = Resolve-Path $dist_path
|
||||
$env:TAURI_DIR = Resolve-Path $src_path
|
||||
|
||||
# build and install everything Rust related.
|
||||
cargo build
|
||||
cargo install cargo-web
|
||||
|
||||
# install the tauri Node CLI and transpile the TS version of the API.
|
||||
cd cli\tauri.js
|
||||
yarn; yarn build;
|
||||
. .scripts/setup.ps1
|
||||
```
|
||||
|
||||
## run
|
||||
@ -93,9 +86,6 @@ $CWD = [Environment]::CurrentDirectory
|
||||
Push-Location $MyInvocation.MyCommand.Path
|
||||
[Environment]::CurrentDirectory = $PWD
|
||||
|
||||
# Invoke the command script.
|
||||
Invoke-Expression -Command .scripts\init_env.ps1
|
||||
|
||||
# get the example paths.
|
||||
$example_path = Get-ChildItem examples\*\*\$env:example
|
||||
|
||||
|
@ -375,8 +375,9 @@ pub struct BuildConfig {
|
||||
}
|
||||
|
||||
fn default_dev_path() -> String {
|
||||
"".to_string()
|
||||
"http://localhost:8080".to_string()
|
||||
}
|
||||
|
||||
fn default_dist_path() -> String {
|
||||
"../dist".to_string()
|
||||
}
|
||||
@ -466,7 +467,7 @@ mod test {
|
||||
|
||||
// create a build config
|
||||
let build = BuildConfig {
|
||||
dev_path: String::from(""),
|
||||
dev_path: String::from("http://localhost:8080"),
|
||||
dist_dir: String::from("../dist"),
|
||||
};
|
||||
|
||||
@ -475,7 +476,7 @@ mod test {
|
||||
assert_eq!(b_config, build);
|
||||
assert_eq!(de_server, tauri.embedded_server);
|
||||
assert_eq!(d_bundle, tauri.bundle);
|
||||
assert_eq!(d_path, String::from(""));
|
||||
assert_eq!(d_path, String::from("http://localhost:8080"));
|
||||
assert_eq!(d_title, tauri.window.title);
|
||||
assert_eq!(d_window, tauri.window);
|
||||
}
|
||||
|
@ -19,11 +19,6 @@ $ npm install
|
||||
$ yarn tauri:build
|
||||
# with npm
|
||||
$ npm run tauri:build
|
||||
# alternatively, if you setup the environment variables, you can build it without Node.js:
|
||||
$ cd ../../..
|
||||
$ . .scripts/init_env.sh
|
||||
$ cd ./tauri/examples/communication
|
||||
$ cargo build --features no-server
|
||||
```
|
||||
|
||||
- Run the app
|
||||
|
@ -19,11 +19,6 @@ $ npm install
|
||||
$ yarn build
|
||||
# with npm
|
||||
$ npm run build
|
||||
# alternatively, if you setup the environment variables, you can build it without Node.js:
|
||||
$ cd ../../..
|
||||
$ . .scripts/init_env.sh
|
||||
$ cd ./tauri/examples/communication
|
||||
$ cargo build --features no-server
|
||||
```
|
||||
|
||||
- Run the app
|
||||
|
Loading…
Reference in New Issue
Block a user