Compare commits
54 Commits
2457545502
...
1d75366f66
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | 1d75366f66 | |
timvisee | 0a849fb7c6 | |
dependabot[bot] | 88725df09d | |
dependabot[bot] | 5a92e7e5e7 | |
dependabot[bot] | 71541fc2b6 | |
timvisee | 5b4c0d2540 | |
Josh | e7f3c91d0b | |
Josh | 8bb198b73e | |
Tim Visée | 9e188bc76c | |
Marian Hähnlein | 1353a54c49 | |
Tim Visée | 4ae007167d | |
Marian Hähnlein | 660f36e584 | |
timvisee | 3dede083cd | |
timvisee | 26e81455ff | |
timvisee | 4ceac20623 | |
timvisee | 073accfe65 | |
timvisee | 6306a433e8 | |
timvisee | 1da317bcc1 | |
timvisee | 08f597405c | |
timvisee | c624766edc | |
Tim Visée | e030c46a9c | |
Marian Hähnlein | d081affa38 | |
Marian Hähnlein | 71372fcbc1 | |
HrBingR | 671390ca24 | |
HrBingR | 9221b86660 | |
HrBingR | fd2e954b3e | |
timvisee | c528ad3147 | |
HrBingR | df9c7ea734 | |
HrBingR | e32ea7d0aa | |
timvisee | 55ad08fd96 | |
timvisee | 96d53e4118 | |
HrBingR | bce861bcaf | |
timvisee | 643287e235 | |
AaronDewes | c619be58ae | |
AaronDewes | 9b8b11ffc3 | |
AaronDewes | 1725ff434e | |
AaronDewes | e1d6224570 | |
AaronDewes | 38746b86fd | |
AaronDewes | 64644b57e3 | |
timvisee | 625fdf5bca | |
AaronDewes | 951c613095 | |
AaronDewes | 16e78847a2 | |
HrBingR | 310271c10f | |
AaronDewes | 55344f8a9d | |
AaronDewes | 2b22e8cd05 | |
AaronDewes | 47ff32fc9f | |
AaronDewes | b598a1c090 | |
AaronDewes | 3ae9e6adeb | |
AaronDewes | 33e7e0f5ba | |
AaronDewes | ca3b5cf7ca | |
AaronDewes | 44a25e4156 | |
timvisee | 000854104f | |
Nam PHAM | 1a0ddf9a05 | |
dependabot[bot] | 0ac1eeed2c |
136
.gitlab-ci.yml
136
.gitlab-ci.yml
|
@ -1,105 +1,71 @@
|
|||
image: "node:15-slim"
|
||||
|
||||
stages:
|
||||
- test
|
||||
- artifact
|
||||
- release
|
||||
|
||||
before_script:
|
||||
# Install dependencies
|
||||
- apt-get update
|
||||
- apt-get install -y git python3 build-essential libxtst6
|
||||
|
||||
# Prepare Chrome for puppeteer
|
||||
- apt-get install -y wget gnupg
|
||||
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
||||
- sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
|
||||
- apt-get update
|
||||
- apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 --no-install-recommends
|
||||
|
||||
# Build Send, run npm tests
|
||||
test:
|
||||
stage: test
|
||||
image: "node:15-slim"
|
||||
only:
|
||||
- api
|
||||
- branches
|
||||
- chat
|
||||
- merge_requests
|
||||
- pushes
|
||||
- schedules
|
||||
- tags
|
||||
- triggers
|
||||
- web
|
||||
before_script:
|
||||
# Install dependencies
|
||||
- apt-get update
|
||||
- apt-get install -y git python3 build-essential libxtst6
|
||||
|
||||
# Prepare Chrome for puppeteer
|
||||
- apt-get install -y wget gnupg
|
||||
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
||||
- sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
|
||||
- apt-get update
|
||||
- apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 --no-install-recommends
|
||||
script:
|
||||
- npm ci
|
||||
- npm run lint
|
||||
- npm test
|
||||
|
||||
# Build Docker image, export Docker image artifact
|
||||
artifact-docker:
|
||||
stage: artifact
|
||||
image: docker:latest
|
||||
needs: []
|
||||
services:
|
||||
- docker:dind
|
||||
variables:
|
||||
IMG_FILE: "send:git-$CI_COMMIT_SHORT_SHA.tar"
|
||||
IMG_NAME: "send:git-$CI_COMMIT_SHORT_SHA"
|
||||
before_script: []
|
||||
script:
|
||||
- docker build -t $IMG_NAME .
|
||||
- docker image save -o $IMG_FILE $IMG_NAME
|
||||
artifacts:
|
||||
name: artifact-docker
|
||||
paths:
|
||||
- $IMG_FILE
|
||||
expire_in: 1 week
|
||||
|
||||
# Release public Docker image for the master branch
|
||||
release-docker-master:
|
||||
stage: release
|
||||
image: docker:latest
|
||||
dependencies:
|
||||
- artifact-docker
|
||||
services:
|
||||
- docker:dind
|
||||
only:
|
||||
- master
|
||||
variables:
|
||||
IMG_IMPORT_FILE: "send:git-$CI_COMMIT_SHORT_SHA.tar"
|
||||
IMG_IMPORT_NAME: "send:git-$CI_COMMIT_SHORT_SHA"
|
||||
IMG_NAME: "registry.gitlab.com/timvisee/send:master-$CI_COMMIT_SHORT_SHA"
|
||||
before_script: []
|
||||
script:
|
||||
# Login in to registry
|
||||
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
|
||||
|
||||
# Load existing, retag for new image images
|
||||
- docker image load -i $IMG_IMPORT_FILE
|
||||
- docker tag $IMG_IMPORT_NAME $IMG_NAME
|
||||
|
||||
# Publish tagged image
|
||||
- docker push $IMG_NAME
|
||||
|
||||
- 'echo "Docker image artifact published, available as:" && echo " docker pull $IMG_NAME"'
|
||||
|
||||
# Release public Docker image for a version tag
|
||||
release-docker:
|
||||
stage: release
|
||||
image: docker:latest
|
||||
dependencies:
|
||||
- artifact-docker
|
||||
services:
|
||||
- docker:dind
|
||||
only:
|
||||
- /^v(\d+\.)*\d+$/
|
||||
variables:
|
||||
IMG_IMPORT_FILE: "send:git-$CI_COMMIT_SHORT_SHA.tar"
|
||||
IMG_IMPORT_NAME: "send:git-$CI_COMMIT_SHORT_SHA"
|
||||
IMG_NAME: "registry.gitlab.com/timvisee/send:$CI_COMMIT_REF_NAME"
|
||||
IMG_NAME_LATEST: "registry.gitlab.com/timvisee/send:latest"
|
||||
before_script: []
|
||||
- api
|
||||
- branches
|
||||
- chat
|
||||
- merge_requests
|
||||
- pushes
|
||||
- schedules
|
||||
- tags
|
||||
- triggers
|
||||
- web
|
||||
script:
|
||||
# Login in to registry
|
||||
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
|
||||
|
||||
# Load existing, retag for new image images
|
||||
- docker image load -i $IMG_IMPORT_FILE
|
||||
- docker tag $IMG_IMPORT_NAME $IMG_NAME
|
||||
- docker tag $IMG_IMPORT_NAME $IMG_NAME_LATEST
|
||||
|
||||
# Publish tagged image
|
||||
- docker push $IMG_NAME
|
||||
- docker push $IMG_NAME_LATEST
|
||||
|
||||
- 'echo "Docker image artifact published, available as:" && echo " docker pull $IMG_NAME_LATEST" && echo " docker pull $IMG_NAME"'
|
||||
- docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
|
||||
- docker build -t send .
|
||||
- |
|
||||
if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
|
||||
IMAGE_NAMES="$CI_REGISTRY_IMAGE/mr:$CI_MERGE_REQUEST_IID"
|
||||
elif [ "$CI_COMMIT_TAG" != "" ]; then
|
||||
IMAGE_NAMES="$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG $CI_REGISTRY_IMAGE:latest"
|
||||
else
|
||||
IMAGE_NAMES="$CI_REGISTRY_IMAGE/$CI_COMMIT_BRANCH:$CI_COMMIT_SHORT_SHA"
|
||||
fi
|
||||
- |
|
||||
for image in $IMAGE_NAMES; do
|
||||
docker tag send $image
|
||||
docker push $image
|
||||
done
|
||||
- |
|
||||
echo "Container image pushed. You can pull it with";
|
||||
for image in $IMAGE_NAMES; do
|
||||
echo "docker pull $image"
|
||||
done
|
||||
|
|
|
@ -11,3 +11,5 @@ rules:
|
|||
selector-list-comma-newline-after: null
|
||||
value-list-comma-newline-after: null
|
||||
at-rule-no-unknown: null
|
||||
# Conflicts with prettier
|
||||
string-quotes: null
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { FluentBundle } from '@fluent/bundle';
|
||||
import { FluentBundle, FluentResource } from '@fluent/bundle';
|
||||
|
||||
function makeBundle(locale, ftl) {
|
||||
const bundle = new FluentBundle(locale, { useIsolating: false });
|
||||
bundle.addMessages(ftl);
|
||||
bundle.addResource(new FluentResource(ftl));
|
||||
return bundle;
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ export async function getTranslator(locale) {
|
|||
return function(id, data) {
|
||||
for (let bundle of bundles) {
|
||||
if (bundle.hasMessage(id)) {
|
||||
return bundle.format(bundle.getMessage(id), data);
|
||||
return bundle.formatPattern(bundle.getMessage(id).value, data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
41
app/main.css
41
app/main.css
|
@ -7,17 +7,14 @@ html {
|
|||
@tailwind components;
|
||||
|
||||
:not(input) {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--violet-gradient: linear-gradient(
|
||||
-180deg,
|
||||
rgba(144, 89, 255, 0.8) 0%,
|
||||
rgba(144, 89, 255, 0.4) 100%
|
||||
rgb(144 89 255 / 80%) 0%,
|
||||
rgb(144 89 255 / 40%) 100%
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -71,7 +68,7 @@ body {
|
|||
|
||||
.checkbox > label::before {
|
||||
/* @apply bg-grey-10; */
|
||||
@apply border;
|
||||
@apply border-default;
|
||||
@apply rounded-sm;
|
||||
|
||||
content: '';
|
||||
|
@ -204,19 +201,18 @@ progress::-webkit-progress-value {
|
|||
background-image: -webkit-linear-gradient(
|
||||
-45deg,
|
||||
transparent 20%,
|
||||
rgba(255, 255, 255, 0.4) 20%,
|
||||
rgba(255, 255, 255, 0.4) 40%,
|
||||
rgb(255 255 255 / 40%) 20%,
|
||||
rgb(255 255 255 / 40%) 40%,
|
||||
transparent 40%,
|
||||
transparent 60%,
|
||||
rgba(255, 255, 255, 0.4) 60%,
|
||||
rgba(255, 255, 255, 0.4) 80%,
|
||||
rgb(255 255 255 / 40%) 60%,
|
||||
rgb(255 255 255 / 40%) 80%,
|
||||
transparent 80%
|
||||
),
|
||||
-webkit-linear-gradient(left, var(--color-primary), var(--color-primary));
|
||||
/* stylelint-enable */
|
||||
border-radius: 2px;
|
||||
background-size: 21px 20px, 100% 100%, 100% 100%;
|
||||
-webkit-animation: animate-stripes 1s linear infinite;
|
||||
}
|
||||
|
||||
progress::-moz-progress-bar {
|
||||
|
@ -224,12 +220,12 @@ progress::-moz-progress-bar {
|
|||
background-image: -moz-linear-gradient(
|
||||
135deg,
|
||||
transparent 20%,
|
||||
rgba(255, 255, 255, 0.4) 20%,
|
||||
rgba(255, 255, 255, 0.4) 40%,
|
||||
rgb(255 255 255 / 40%) 20%,
|
||||
rgb(255 255 255 / 40%) 40%,
|
||||
transparent 40%,
|
||||
transparent 60%,
|
||||
rgba(255, 255, 255, 0.4) 60%,
|
||||
rgba(255, 255, 255, 0.4) 80%,
|
||||
rgb(255 255 255 / 40%) 60%,
|
||||
rgb(255 255 255 / 40%) 80%,
|
||||
transparent 80%
|
||||
),
|
||||
-moz-linear-gradient(left, var(--color-primary), var(--color-primary));
|
||||
|
@ -239,12 +235,6 @@ progress::-moz-progress-bar {
|
|||
animation: animate-stripes 1s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animate-stripes {
|
||||
100% {
|
||||
background-position: -21px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animate-stripes {
|
||||
100% {
|
||||
background-position: -21px 0;
|
||||
|
@ -313,7 +303,7 @@ select {
|
|||
|
||||
@screen md {
|
||||
.main > section {
|
||||
@apply border;
|
||||
@apply border-default;
|
||||
@apply border-grey-80;
|
||||
}
|
||||
}
|
||||
|
@ -323,13 +313,12 @@ select {
|
|||
|
||||
@responsive {
|
||||
.shadow-light {
|
||||
box-shadow: 0 0 8px 0 rgba(12, 12, 13, 0.1);
|
||||
box-shadow: 0 0 8px 0 rgb(12 12 13 / 10%);
|
||||
}
|
||||
|
||||
.shadow-big {
|
||||
box-shadow: 0 12px 18px 2px rgba(34, 0, 51, 0.04),
|
||||
0 6px 22px 4px rgba(7, 48, 114, 0.12),
|
||||
0 6px 10px -4px rgba(14, 13, 26, 0.12);
|
||||
box-shadow: 0 12px 18px 2px rgb(34 0 51 / 4%),
|
||||
0 6px 22px 4px rgb(7 48 114 / 12%), 0 6px 10px -4px rgb(14 13 26 / 12%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -83,13 +83,13 @@ class Account extends Component {
|
|||
<input
|
||||
type="image"
|
||||
alt="${user.email}"
|
||||
class="w-8 h-8 rounded-full border text-primary md:text-white focus:outline"
|
||||
class="w-8 h-8 rounded-full border-default text-primary md:text-white focus:outline"
|
||||
src="${user.avatar}"
|
||||
onclick="${e => this.avatarClick(e)}"
|
||||
/>
|
||||
<ul
|
||||
id="accountMenu"
|
||||
class="invisible absolute top-0 right-0 mt-10 pt-2 pb-2 bg-white shadow-md whitespace-no-wrap outline-none z-50 dark:bg-grey-80"
|
||||
class="invisible absolute top-0 right-0 mt-10 pt-2 pb-2 bg-white shadow-md whitespace-nowrap outline-none z-50 dark:bg-grey-80"
|
||||
onblur="${e => this.hideMenu(e)}"
|
||||
>
|
||||
<li class="p-2 text-grey-60 dark:text-grey-50">${user.email}</li>
|
||||
|
|
|
@ -53,7 +53,7 @@ function password(state) {
|
|||
id="password-input"
|
||||
class="${state.archive.password
|
||||
? ''
|
||||
: 'invisible'} border rounded focus:border-primary leading-normal my-1 py-1 px-2 h-8 dark:bg-grey-80"
|
||||
: 'invisible'} border-default rounded-default focus:border-primary leading-normal my-1 py-1 px-2 h-8 dark:bg-grey-80"
|
||||
autocomplete="off"
|
||||
maxlength="${MAX_LENGTH}"
|
||||
type="password"
|
||||
|
@ -261,7 +261,7 @@ module.exports = function(state, emit, archive) {
|
|||
return html`
|
||||
<send-archive
|
||||
id="archive-${archive.id}"
|
||||
class="flex flex-col items-start rounded shadow-light bg-white p-4 w-full dark:bg-grey-90 dark:border dark:border-grey-70"
|
||||
class="flex flex-col items-start rounded-default shadow-light bg-white p-4 w-full dark:bg-grey-90 dark:border-default dark:border-grey-70"
|
||||
>
|
||||
${archiveInfo(
|
||||
archive,
|
||||
|
@ -335,7 +335,7 @@ module.exports.wip = function(state, emit) {
|
|||
fileInfo(f, remove(f, state.translate('deleteButtonHover')))
|
||||
),
|
||||
'flex-shrink bg-grey-10 rounded-t overflow-y-auto px-6 py-4 md:h-full md:max-h-half-screen dark:bg-black',
|
||||
'bg-white px-2 my-2 shadow-light rounded dark:bg-grey-90 dark:border dark:border-grey-80'
|
||||
'bg-white px-2 my-2 shadow-light rounded-default dark:bg-grey-90 dark:border-default dark:border-grey-80'
|
||||
)}
|
||||
<div
|
||||
class="flex-shrink-0 flex-grow flex items-end p-4 bg-grey-10 rounded-b mb-1 font-medium dark:bg-grey-90"
|
||||
|
@ -438,7 +438,7 @@ module.exports.uploading = function(state, emit) {
|
|||
return html`
|
||||
<send-upload-area
|
||||
id="${archive.id}"
|
||||
class="flex flex-col items-start rounded shadow-light bg-white p-4 w-full dark:bg-grey-90"
|
||||
class="flex flex-col items-start rounded-default shadow-light bg-white p-4 w-full dark:bg-grey-90"
|
||||
>
|
||||
${archiveInfo(archive)}
|
||||
<div class="text-xs opacity-75 w-full mt-2 mb-2">
|
||||
|
@ -488,7 +488,7 @@ module.exports.empty = function(state, emit) {
|
|||
`;
|
||||
return html`
|
||||
<send-upload-area
|
||||
class="flex flex-col items-center justify-center border-2 border-dashed border-grey-transparent rounded px-6 py-16 h-full w-full dark:border-grey-60"
|
||||
class="flex flex-col items-center justify-center border-2 border-dashed border-grey-transparent rounded-default px-6 py-16 h-full w-full dark:border-grey-60"
|
||||
onclick="${e => {
|
||||
if (e.target.tagName !== 'LABEL') {
|
||||
document.getElementById('file-upload').click();
|
||||
|
@ -563,7 +563,7 @@ module.exports.preview = function(state, emit) {
|
|||
<send-archive
|
||||
class="flex flex-col max-h-full bg-white p-4 w-full md:w-128 dark:bg-grey-90"
|
||||
>
|
||||
<div class="border rounded py-3 px-6 dark:border-grey-70">
|
||||
<div class="border-default rounded-default py-3 px-6 dark:border-grey-70">
|
||||
${archiveInfo(archive)} ${details}
|
||||
</div>
|
||||
<button
|
||||
|
@ -590,7 +590,7 @@ module.exports.downloading = function(state) {
|
|||
const progressPercent = percent(progress);
|
||||
return html`
|
||||
<send-archive
|
||||
class="flex flex-col bg-white rounded shadow-light p-4 w-full max-w-sm md:w-128 dark:bg-grey-90"
|
||||
class="flex flex-col bg-white rounded-default shadow-light p-4 w-full max-w-sm md:w-128 dark:bg-grey-90"
|
||||
>
|
||||
${archiveInfo(archive)}
|
||||
<div class="link-primary text-sm font-medium mt-2">
|
||||
|
|
|
@ -21,7 +21,7 @@ module.exports = function(name, url) {
|
|||
<input
|
||||
type="text"
|
||||
id="share-url"
|
||||
class="block w-full my-4 border rounded-lg leading-loose h-12 px-2 py-1 dark:bg-grey-80"
|
||||
class="block w-full my-4 border-default rounded-lg leading-loose h-12 px-2 py-1 dark:bg-grey-80"
|
||||
value="${url}"
|
||||
readonly="true"
|
||||
/>
|
||||
|
|
|
@ -17,7 +17,7 @@ module.exports = function(state, emit) {
|
|||
${state.translate('downloadDescription')}
|
||||
</p>
|
||||
<form
|
||||
class="flex flex-row flex-no-wrap w-full md:w-4/5"
|
||||
class="flex flex-row flex-nowrap w-full md:w-4/5"
|
||||
onsubmit="${checkPassword}"
|
||||
data-no-csrf
|
||||
>
|
||||
|
|
|
@ -65,6 +65,45 @@ class Footer extends Component {
|
|||
`);
|
||||
}
|
||||
|
||||
// Defining a custom footer
|
||||
var footer = [];
|
||||
if (this.state != undefined && this.state.WEB_UI != undefined) {
|
||||
const WEB_UI = this.state.WEB_UI;
|
||||
|
||||
if (WEB_UI.CUSTOM_FOOTER_URL != '' && WEB_UI.CUSTOM_FOOTER_TEXT != '') {
|
||||
footer.push(html`
|
||||
<li class="m-2">
|
||||
<a href="${WEB_UI.CUSTOM_FOOTER_URL}" target="_blank">
|
||||
${WEB_UI.CUSTOM_FOOTER_TEXT}
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
else if (WEB_UI.CUSTOM_FOOTER_URL != '') {
|
||||
footer.push(html`
|
||||
<li class="m-2">
|
||||
<a href="${WEB_UI.CUSTOM_FOOTER_URL}" target="_blank">
|
||||
${WEB_UI.CUSTOM_FOOTER_URL}
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
else if (WEB_UI.CUSTOM_FOOTER_TEXT != '') {
|
||||
footer.push(html`
|
||||
<li class="m-2">
|
||||
${WEB_UI.CUSTOM_FOOTER_TEXT}
|
||||
</li>
|
||||
`)
|
||||
}
|
||||
else {
|
||||
footer.push(html`
|
||||
<li class="m-2">
|
||||
${translate('footerText')}
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<footer
|
||||
class="flex flex-col md:flex-row items-start w-full flex-none self-start p-6 md:p-8 font-medium text-xs text-grey-60 dark:text-grey-40 md:items-center justify-between"
|
||||
|
@ -72,7 +111,7 @@ class Footer extends Component {
|
|||
<ul
|
||||
class="flex flex-col md:flex-row items-start md:items-center md:justify-start"
|
||||
>
|
||||
<li class="m-2">${translate('footerText')}</li>
|
||||
${footer}
|
||||
</ul>
|
||||
<ul
|
||||
class="flex flex-col md:flex-row items-start md:items-center md:justify-end"
|
||||
|
|
|
@ -12,12 +12,12 @@ module.exports = function(state, emit) {
|
|||
'downloadTitle'
|
||||
)}</h1>
|
||||
<p
|
||||
class="w-full p-2 border border-yellow-50 rounded md:w-4/5 text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
class="w-full p-2 border-default border-yellow-50 rounded-default md:w-4/5 text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
>
|
||||
⚠️ ${state.translate('noStreamsWarning')} ⚠️
|
||||
</p>
|
||||
<form class="md:w-128" onsubmit=${submit}>
|
||||
<fieldset class="border rounded p-4 my-4" onchange=${optionChanged}>
|
||||
<fieldset class="border-default rounded-default p-4 my-4" onchange=${optionChanged}>
|
||||
<div class="flex items-center mb-2">
|
||||
<svg class="h-8 w-6 mr-3 flex-shrink-0 text-primary">
|
||||
<use xlink:href="${assets.get('blue_file.svg')}#icon"/>
|
||||
|
|
|
@ -18,7 +18,7 @@ module.exports = function(selected, options, translate, changed, htmlId) {
|
|||
return html`
|
||||
<select
|
||||
id="${htmlId}"
|
||||
class="appearance-none cursor-pointer border rounded bg-grey-10 hover:border-primary focus:border-primary pl-1 pr-8 py-1 my-1 h-8 dark:bg-grey-80"
|
||||
class="appearance-none cursor-pointer border-default rounded-default bg-grey-10 hover:border-primary focus:border-primary pl-1 pr-8 py-1 my-1 h-8 dark:bg-grey-80"
|
||||
data-selected="${selected}"
|
||||
onchange="${choose}"
|
||||
>
|
||||
|
|
|
@ -18,7 +18,7 @@ module.exports = function(name, url) {
|
|||
<input
|
||||
type="text"
|
||||
id="share-url"
|
||||
class="w-full my-4 border rounded-lg leading-loose h-12 px-2 py-1 dark:bg-grey-80"
|
||||
class="w-full my-4 border-default rounded-lg leading-loose h-12 px-2 py-1 dark:bg-grey-80"
|
||||
value="${url}"
|
||||
readonly="true"
|
||||
/>
|
||||
|
|
|
@ -35,7 +35,7 @@ module.exports = function() {
|
|||
<input
|
||||
id="email-input"
|
||||
type="email"
|
||||
class="hidden border rounded-lg w-full px-2 py-1 h-12 mb-3 text-lg text-grey-70 leading-loose dark:bg-grey-80 dark:text-white"
|
||||
class="hidden border-default rounded-lg w-full px-2 py-1 h-12 mb-3 text-lg text-grey-70 leading-loose dark:bg-grey-80 dark:text-white"
|
||||
placeholder=${state.translate('emailPlaceholder')}
|
||||
/>
|
||||
<input
|
||||
|
|
|
@ -27,7 +27,7 @@ module.exports = function(state, emit) {
|
|||
<main class="main">
|
||||
${state.modal && modal(state, emit)}
|
||||
<section
|
||||
class="flex flex-col items-center justify-center text-center bg-white m-6 px-6 py-8 border border-grey-30 md:border-none md:px-12 md:py-16 shadow w-full md:h-full dark:bg-grey-90"
|
||||
class="flex flex-col items-center justify-center text-center bg-white m-6 px-6 py-8 border-default border-grey-30 md:border-none md:px-12 md:py-16 shadow-default w-full md:h-full dark:bg-grey-90"
|
||||
>
|
||||
<h1 class="text-3xl font-bold">${strings.header}</h1>
|
||||
<p class="mt-4 mb-8 max-w-md leading-normal">${strings.description}</p>
|
||||
|
|
|
@ -96,6 +96,11 @@ See the table below for the variables and their default values.
|
|||
| UI_CUSTOM_ASSETS_FACEBOOK | | A custom header image for Facebook |
|
||||
| UI_CUSTOM_ASSETS_TWITTER | | A custom header image for Twitter |
|
||||
| UI_CUSTOM_ASSETS_WORDMARK | | A custom wordmark (Text next to the logo) |
|
||||
| UI_CUSTOM_CSS | | Allows you to define a custom CSS file for custom styling |
|
||||
| CUSTOM_FOOTER_TEXT | | Allows you to define a custom footer |
|
||||
| CUSTOM_FOOTER_URL | | Allows you to define a custom URL in your footer |
|
||||
|
||||
Side note: If you define a custom URL and a custom footer, only the footer text will display, but will be hyperlinked to the URL.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "send",
|
||||
"description": "File Sharing Experiment",
|
||||
"version": "3.4.20",
|
||||
"version": "3.4.21",
|
||||
"author": "Mozilla (https://mozilla.org)",
|
||||
"contributors": [
|
||||
"Tim Visee <3a4fb3964f@sinenomine.email> (https://timvisee.com)"
|
||||
|
@ -67,7 +67,7 @@
|
|||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@dannycoates/webcrypto-liner": "^0.1.37",
|
||||
"@fullhuman/postcss-purgecss": "^1.3.0",
|
||||
"@fullhuman/postcss-purgecss": "^4.1.3",
|
||||
"@mattiasbuelens/web-streams-polyfill": "0.2.1",
|
||||
"@sentry/browser": "^5.30.0",
|
||||
"asmcrypto.js": "^0.22.0",
|
||||
|
@ -75,13 +75,13 @@
|
|||
"babel-plugin-istanbul": "^5.2.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"content-disposition": "^0.5.4",
|
||||
"copy-webpack-plugin": "^5.1.2",
|
||||
"copy-webpack-plugin": "^6.4.0",
|
||||
"core-js": "^3.21.1",
|
||||
"crc": "^3.8.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"css-loader": "^3.6.0",
|
||||
"css-loader": "^5.2.7",
|
||||
"css-mqpacker": "^7.0.0",
|
||||
"cssnano": "^4.1.11",
|
||||
"cssnano": "^5.1.12",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-mocha": "^6.2.1",
|
||||
|
@ -91,22 +91,23 @@
|
|||
"extract-loader": "^3.2.0",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"fast-text-encoding": "^1.0.3",
|
||||
"file-loader": "^4.2.0",
|
||||
"git-rev-sync": "^1.12.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"git-rev-sync": "^3.0.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"http_ece": "^1.1.0",
|
||||
"husky": "^3.0.9",
|
||||
"intl-pluralrules": "^1.3.1",
|
||||
"lint-staged": "^9.4.2",
|
||||
"mocha": "^6.2.2",
|
||||
"mocha": "^10.1.0",
|
||||
"morgan": "^1.9.1",
|
||||
"nanobus": "^4.5.0",
|
||||
"nanohtml": "^1.9.0",
|
||||
"nanotiming": "^7.3.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^14.1.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-preset-env": "^6.7.1",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-loader": "^4.2.0",
|
||||
"postcss-preset-env": "^7.7.2",
|
||||
"prettier": "^1.19.1",
|
||||
"proxyquire": "^2.1.3",
|
||||
"puppeteer": "^2.0.0",
|
||||
|
@ -115,13 +116,13 @@
|
|||
"script-loader": "^0.7.2",
|
||||
"sinon": "^7.5.0",
|
||||
"string-hash": "^1.1.3",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^19.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^4.1.4",
|
||||
"svgo": "^1.3.2",
|
||||
"svgo-loader": "^2.2.2",
|
||||
"tailwindcss": "^1.9.6",
|
||||
"val-loader": "^1.1.1",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^5.0.3",
|
||||
"svgo": "^2.8.0",
|
||||
"svgo-loader": "^3.0.1",
|
||||
"tailwindcss": "^2",
|
||||
"val-loader": "^2.1.2",
|
||||
"webpack": "4.38.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-middleware": "^3.7.3",
|
||||
|
@ -131,10 +132,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@dannycoates/express-ws": "^5.0.3",
|
||||
"@fluent/bundle": "^0.13.0",
|
||||
"@fluent/langneg": "^0.3.0",
|
||||
"@google-cloud/storage": "^5.19.0",
|
||||
"@sentry/node": "^5.30.0",
|
||||
"@fluent/bundle": "^0.17.1",
|
||||
"@fluent/langneg": "^0.6.2",
|
||||
"@google-cloud/storage": "^6.2.3",
|
||||
"@sentry/node": "^7.7.0",
|
||||
"aws-sdk": "^2.1109.0",
|
||||
"body-parser": "^1.20.0",
|
||||
"choo": "^7.0.0",
|
||||
|
@ -145,8 +146,7 @@
|
|||
"double-ended-queue": "^2.1.0-0",
|
||||
"express": "^4.17.3",
|
||||
"helmet": "^3.23.3",
|
||||
"mkdirp": "^0.5.6",
|
||||
"mozlog": "^2.2.0",
|
||||
"mozlog": "^3.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"redis": "^3.1.1",
|
||||
"redis-mock": "^0.47.0",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
class TailwindExtractor {
|
||||
static extract(content) {
|
||||
return content.match(/[A-Za-z0-9-_:/]+/g) || [];
|
||||
}
|
||||
}
|
||||
const TailwindExtractor = content => {
|
||||
return content.match(/[A-Za-z0-9-_:/]+/g) || [];
|
||||
};
|
||||
|
||||
const options = {
|
||||
plugins: [
|
||||
|
|
|
@ -13,6 +13,8 @@ module.exports = {
|
|||
FOOTER_CLI_URL: config.footer_cli_url,
|
||||
FOOTER_DMCA_URL: config.footer_dmca_url,
|
||||
FOOTER_SOURCE_URL: config.footer_source_url,
|
||||
CUSTOM_FOOTER_TEXT: config.custom_footer_text,
|
||||
CUSTOM_FOOTER_URL: config.custom_footer_url,
|
||||
COLORS: {
|
||||
PRIMARY: config.ui_color_primary,
|
||||
ACCENT: config.ui_color_accent
|
||||
|
|
|
@ -165,9 +165,19 @@ const conf = convict({
|
|||
},
|
||||
base_url: {
|
||||
format: 'url',
|
||||
default: 'https://send.firefox.com',
|
||||
default: 'https://send.example.com',
|
||||
env: 'BASE_URL'
|
||||
},
|
||||
custom_title: {
|
||||
format: String,
|
||||
default: 'Send',
|
||||
env: 'CUSTOM_TITLE'
|
||||
},
|
||||
custom_description: {
|
||||
format: String,
|
||||
default: 'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.',
|
||||
env: 'CUSTOM_DESCRIPTION'
|
||||
},
|
||||
detect_base_url: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
|
@ -243,6 +253,16 @@ const conf = convict({
|
|||
default: 'https://github.com/timvisee/send',
|
||||
env: 'SEND_FOOTER_SOURCE_URL'
|
||||
},
|
||||
custom_footer_text: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'CUSTOM_FOOTER_TEXT'
|
||||
},
|
||||
custom_footer_url: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'CUSTOM_FOOTER_URL'
|
||||
},
|
||||
ui_color_primary: {
|
||||
format: String,
|
||||
default: '#0a84ff',
|
||||
|
@ -253,6 +273,11 @@ const conf = convict({
|
|||
default: '#003eaa',
|
||||
env: 'UI_COLOR_ACCENT'
|
||||
},
|
||||
custom_locale: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'CUSTOM_LOCALE'
|
||||
},
|
||||
ui_custom_assets: {
|
||||
android_chrome_192px: {
|
||||
format: String,
|
||||
|
@ -303,6 +328,11 @@ const conf = convict({
|
|||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_WORDMARK'
|
||||
},
|
||||
custom_css: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_CSS'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,6 +3,10 @@ const assets = require('../common/assets');
|
|||
const initScript = require('./initScript');
|
||||
|
||||
module.exports = function(state, body = '') {
|
||||
const custom_css = state.ui.assets.custom_css !== ''
|
||||
? html`<link rel="stylesheet" type="text/css" href="${state.ui.assets.custom_css}" />`
|
||||
: ''
|
||||
|
||||
return html`
|
||||
<!DOCTYPE html>
|
||||
<html lang="${state.locale}">
|
||||
|
@ -40,6 +44,7 @@ module.exports = function(state, body = '') {
|
|||
type="text/css"
|
||||
href="${assets.get('app.css')}"
|
||||
/>
|
||||
${custom_css}
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { FluentBundle } = require('@fluent/bundle');
|
||||
const { FluentBundle, FluentResource } = require('@fluent/bundle');
|
||||
const localesPath = path.resolve(__dirname, '../public/locales');
|
||||
const locales = fs.readdirSync(localesPath);
|
||||
|
||||
function makeBundle(locale) {
|
||||
const bundle = new FluentBundle(locale, { useIsolating: false });
|
||||
bundle.addMessages(
|
||||
fs.readFileSync(path.resolve(localesPath, locale, 'send.ftl'), 'utf8')
|
||||
bundle.addResource(
|
||||
new FluentResource(
|
||||
fs.readFileSync(path.resolve(localesPath, locale, 'send.ftl'), 'utf8')
|
||||
)
|
||||
);
|
||||
return [locale, bundle];
|
||||
}
|
||||
|
@ -19,8 +21,11 @@ module.exports = function getTranslator(locale) {
|
|||
const bundle = bundles.get(locale) || defaultBundle;
|
||||
return function(id, data) {
|
||||
if (bundle.hasMessage(id)) {
|
||||
return bundle.format(bundle.getMessage(id), data);
|
||||
return bundle.formatPattern(bundle.getMessage(id).value, data);
|
||||
}
|
||||
return defaultBundle.format(defaultBundle.getMessage(id), data);
|
||||
return defaultBundle.formatPattern(
|
||||
defaultBundle.getMessage(id).value,
|
||||
data
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const assert = require('assert');
|
||||
const crypto = require('crypto');
|
||||
const storage = require('../storage');
|
||||
const config = require('../config');
|
||||
const fxa = require('../fxa');
|
||||
|
||||
module.exports = {
|
||||
|
@ -70,10 +71,11 @@ module.exports = {
|
|||
const token = authHeader.split(' ')[1];
|
||||
req.user = await fxa.verify(token);
|
||||
}
|
||||
if (req.user) {
|
||||
next();
|
||||
} else {
|
||||
|
||||
if (config.fxa_required && !req.user) {
|
||||
res.sendStatus(401);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,9 +3,18 @@ const layout = require('./layout');
|
|||
const assets = require('../common/assets');
|
||||
const getTranslator = require('./locale');
|
||||
const { getFxaConfig } = require('./fxa');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = async function(req) {
|
||||
const locale = req.language || 'en-US';
|
||||
const locale = (() => {
|
||||
if (config.custom_locale != '' && fs.existsSync(path.join(__dirname,'../public/locales',config.custom_locale))) {
|
||||
return config.custom_locale;
|
||||
}
|
||||
else {
|
||||
return req.language || 'en-US';
|
||||
}
|
||||
})();
|
||||
let authConfig = null;
|
||||
let robots = 'none';
|
||||
if (req.route && req.route.path === '/') {
|
||||
|
@ -34,7 +43,8 @@ module.exports = async function(req) {
|
|||
safari_pinned_tab: assets.get('safari-pinned-tab.svg'),
|
||||
facebook: baseUrl + '/' + assets.get('send-fb.jpg'),
|
||||
twitter: baseUrl + '/' + assets.get('send-twitter.jpg'),
|
||||
wordmark: assets.get('wordmark.svg') + '#logo'
|
||||
wordmark: assets.get('wordmark.svg') + '#logo',
|
||||
custom_css: ''
|
||||
};
|
||||
Object.keys(uiAssets).forEach(index => {
|
||||
if (config.ui_custom_assets[index] !== '')
|
||||
|
@ -47,9 +57,8 @@ module.exports = async function(req) {
|
|||
locale,
|
||||
capabilities: { account: false },
|
||||
translate: getTranslator(locale),
|
||||
title: 'Send',
|
||||
description:
|
||||
'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.',
|
||||
title: config.custom_title,
|
||||
description: config.custom_description,
|
||||
baseUrl,
|
||||
ui: {
|
||||
colors: {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const promisify = require('util').promisify;
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
const stat = promisify(fs.stat);
|
||||
|
||||
|
@ -9,7 +8,9 @@ class FSStorage {
|
|||
constructor(config, log) {
|
||||
this.log = log;
|
||||
this.dir = config.file_dir;
|
||||
mkdirp.sync(this.dir);
|
||||
fs.mkdirSync(this.dir, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
async length(id) {
|
||||
|
|
|
@ -23,6 +23,7 @@ const colors = {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
purge: false,
|
||||
theme: {
|
||||
colors: colors,
|
||||
screens: {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable no-undef, no-process-exit */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const puppeteer = require('puppeteer');
|
||||
const webpack = require('webpack');
|
||||
const config = require('../../webpack.config');
|
||||
|
@ -44,7 +43,9 @@ const server = app.listen(async function() {
|
|||
const coverage = await page.evaluate(() => __coverage__);
|
||||
if (coverage) {
|
||||
const dir = path.resolve(__dirname, '../../.nyc_output');
|
||||
mkdirp.sync(dir);
|
||||
fs.mkdirSync(dir, {
|
||||
recursive: true
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.resolve(dir, 'frontend.json'),
|
||||
JSON.stringify(coverage)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const fs = require('fs');
|
||||
const rimraf = require('rimraf');
|
||||
const dir = path.join(__dirname, 'integration', 'downloads');
|
||||
|
||||
mkdirp.sync(dir);
|
||||
fs.mkdirSync(dir, {
|
||||
recursive: true
|
||||
});
|
||||
rimraf.sync(`${dir}${path.sep}*`);
|
||||
|
||||
exports.config = {
|
||||
|
|
|
@ -42,7 +42,8 @@ const serviceWorker = {
|
|||
test: /\.(png|jpg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash:8].[ext]'
|
||||
name: '[name].[contenthash:8].[ext]',
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -51,16 +52,26 @@ const serviceWorker = {
|
|||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash:8].[ext]'
|
||||
name: '[name].[contenthash:8].[ext]',
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'svgo-loader',
|
||||
options: {
|
||||
plugins: [
|
||||
{ removeViewBox: false }, // true causes stretched images
|
||||
{ convertStyleToAttrs: true }, // for CSP, no unsafe-eval
|
||||
{ removeTitle: true } // for smallness
|
||||
{
|
||||
name: 'removeViewBox',
|
||||
active: false // true causes stretched images
|
||||
},
|
||||
{
|
||||
name: 'convertStyleToAttrs',
|
||||
active: true // for CSP, no unsafe-eval
|
||||
},
|
||||
{
|
||||
name: 'removeTitle',
|
||||
active: true // for smallness
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +138,8 @@ const web = {
|
|||
test: /\.(png|jpg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash:8].[ext]'
|
||||
name: '[name].[contenthash:8].[ext]',
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -136,17 +148,30 @@ const web = {
|
|||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash:8].[ext]'
|
||||
name: '[name].[contenthash:8].[ext]',
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'svgo-loader',
|
||||
options: {
|
||||
plugins: [
|
||||
{ cleanupIDs: false },
|
||||
{ removeViewBox: false }, // true causes stretched images
|
||||
{ convertStyleToAttrs: true }, // for CSP, no unsafe-eval
|
||||
{ removeTitle: true } // for smallness
|
||||
{
|
||||
name: 'cleanupIDs',
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: 'removeViewBox',
|
||||
active: false // true causes stretched images
|
||||
},
|
||||
{
|
||||
name: 'convertStyleToAttrs',
|
||||
active: true // for CSP, no unsafe-eval
|
||||
},
|
||||
{
|
||||
name: 'removeTitle',
|
||||
active: true // for smallness
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +185,8 @@ const web = {
|
|||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1
|
||||
importLoaders: 1,
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
'postcss-loader'
|
||||
|
@ -184,12 +210,14 @@ const web = {
|
|||
]
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin([
|
||||
{
|
||||
context: 'public',
|
||||
from: '*.*'
|
||||
}
|
||||
]),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
context: 'public',
|
||||
from: '*.*'
|
||||
}
|
||||
]
|
||||
}),
|
||||
new webpack.EnvironmentPlugin(['NODE_ENV']),
|
||||
new webpack.IgnorePlugin(/\.\.\/dist/), // used in common/*.js
|
||||
new ExtractTextPlugin({
|
||||
|
|
Loading…
Reference in New Issue