Compare commits
186 Commits
Author | SHA1 | Date | |
---|---|---|---|
c8c39b0f83 | |||
61ea7b2940 | |||
4d4e6eaeb7 | |||
2debd4c3f3 | |||
c7885e3a90 | |||
8c54585f2d | |||
e067894e02 | |||
407a85fffb | |||
e0366cf1ef | |||
04a0ce17de | |||
db87229f16 | |||
3df319e071 | |||
ad5abb6054 | |||
ac822a2531 | |||
e25e9c2b24 | |||
1666693c25 | |||
d17a7e8d82 | |||
cdb9b185f2 | |||
76b44b949c | |||
950cb118cc | |||
3703d2b73d | |||
fac8700192 | |||
04cd943eb4 | |||
472a3c3355 | |||
5d41048695 | |||
42ee7477a0 | |||
bbfa0d5834 | |||
4885c2d69e | |||
b65df3e3bf | |||
57e89bb1cc | |||
19af921913 | |||
5ba4019e36 | |||
e3963736ad | |||
b9f31cc19c | |||
7c6029aa99 | |||
cfc7b15211 | |||
70e84c5840 | |||
1505abb703 | |||
758879ef84 | |||
afd0925cfc | |||
7d3a888c79 | |||
cfbda6ec97 | |||
44e891331e | |||
8ab2943126 | |||
fa45619dbb | |||
ec69eb2dcb | |||
882daddb51 | |||
a9fd6ac114 | |||
fd17a282fa | |||
9f26b27d5d | |||
d06740aa96 | |||
da86a0931c | |||
b6d86aab30 | |||
03c74c88b9 | |||
ca6c24dee1 | |||
057ab2d886 | |||
ccbfe93765 | |||
fd43f84bd0 | |||
72da075972 | |||
2e5d3f5624 | |||
9394b361bd | |||
e9912ea87f | |||
2afc356911 | |||
4c1c96b163 | |||
478984e944 | |||
e045bd8a1e | |||
06e25df962 | |||
777f7b4c37 | |||
deabba80ca | |||
29f67fad06 | |||
c0ca073b2d | |||
de4f6b45e5 | |||
0bca8c9d02 | |||
e3e3cf9d22 | |||
2be071d215 | |||
5cd09e6c53 | |||
537d131883 | |||
a792e4ea87 | |||
7312c17f6a | |||
b0017d134f | |||
796d05925a | |||
86ab5edf4b | |||
1edeb717b1 | |||
43fe80c82e | |||
8c9c348615 | |||
32f4dc5f2a | |||
523289523d | |||
45b0657858 | |||
629369487c | |||
3c17988b24 | |||
2ae54dea64 | |||
45b1aec1d9 | |||
861f4b02d1 | |||
e91836e9bb | |||
6882d102cc | |||
6114b5b06b | |||
934ac9d4e0 | |||
429122574f | |||
fe5a2f9ee7 | |||
7d71fd2bc6 | |||
9c4404963b | |||
c15daea875 | |||
25aab1d7af | |||
c054df072b | |||
96fd19b178 | |||
7ec0dea7bf | |||
d420c48f5f | |||
2c133599bd | |||
b88e3e349a | |||
b4e3bd6d1f | |||
676f36c973 | |||
c48309b648 | |||
2aec417fa8 | |||
f97ed8a65b | |||
1c1f638be7 | |||
3dce6b1204 | |||
58b2812eaa | |||
5a42e06510 | |||
4a9342bc50 | |||
c497b7325f | |||
072d294468 | |||
0062fc28aa | |||
b963279296 | |||
af9289866d | |||
031a2f0816 | |||
2189e4fd49 | |||
130832177c | |||
915d050109 | |||
e0f921473a | |||
d3af6f477c | |||
9550220e01 | |||
c59551da40 | |||
12c92cb55a | |||
87082bb978 | |||
ba3368a3eb | |||
d43cf8c58b | |||
6d23950849 | |||
807b6b9f7b | |||
62cdfdf4fe | |||
379c93c303 | |||
096afcd6ae | |||
f7aea7ad4d | |||
0cfc87775e | |||
58509cbfc9 | |||
2cc6df22d1 | |||
c1000f7511 | |||
e4d7f57bd7 | |||
74ce409076 | |||
54a152ddad | |||
9f3086a05a | |||
159e3ec99b | |||
7940741569 | |||
1e210a539f | |||
24b5653c7c | |||
c32680a643 | |||
b7631c6d97 | |||
50a85c6b39 | |||
f37a2347e1 | |||
52b2b6ed69 | |||
f5252e39b2 | |||
8012fc1f02 | |||
e3d3911832 | |||
5be8319d8a | |||
f4682d3f1c | |||
7d1fa28d88 | |||
f08ed0fc28 | |||
22fd5ee1c9 | |||
c00c30e211 | |||
42fdc43ff5 | |||
8db06a7625 | |||
8217321ecb | |||
da200698dd | |||
7d2c3526b2 | |||
15a8381f04 | |||
d3711ab3ba | |||
23ad1cf882 | |||
65b03d9fbb | |||
3fae30b182 | |||
6e2399eb7c | |||
f09a382c6d | |||
3a61d3aa3a | |||
c97b1fc454 | |||
a213ae400a | |||
fed6ddbd4e | |||
a8542bc222 | |||
2d99586d4a |
@ -2,7 +2,7 @@
|
||||
|
||||
If you want to contribute, but do not want to install anything on your host system, we got you covered. You only need docker. Technically, it could be done using vs code development in container, but this is more fun:).
|
||||
|
||||
You need to copy `.env.development` to `.env` and customize values.
|
||||
You need to copy `.env.default` to `.env` and customize values.
|
||||
|
||||
## Step 1: Building server
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# STAGE 1: SERVER
|
||||
#
|
||||
FROM golang:1.17-bullseye as server
|
||||
FROM golang:1.18-bullseye as server
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
@ -13,7 +13,7 @@ RUN set -eux; apt-get update; \
|
||||
# install libclipboard
|
||||
set -eux; \
|
||||
cd /tmp; \
|
||||
git clone https://github.com/jtanx/libclipboard; \
|
||||
git clone --depth=1 https://github.com/jtanx/libclipboard; \
|
||||
cd libclipboard; \
|
||||
cmake .; \
|
||||
make -j4; \
|
||||
@ -60,19 +60,35 @@ ARG USERNAME=neko
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
|
||||
RUN set -eux; \
|
||||
#
|
||||
# add non-free repo for intel drivers
|
||||
echo deb http://deb.debian.org/debian bullseye main contrib non-free > /etc/apt/sources.list; \
|
||||
echo deb http://deb.debian.org/debian-security/ bullseye-security main contrib non-free >> /etc/apt/sources.list; \
|
||||
echo deb http://deb.debian.org/debian bullseye-updates main contrib non-free >> /etc/apt/sources.list; \
|
||||
apt-get update; \
|
||||
#
|
||||
# install dependencies
|
||||
RUN set -eux; apt-get update; \
|
||||
apt-get install -y --no-install-recommends wget ca-certificates supervisor; \
|
||||
apt-get install -y --no-install-recommends pulseaudio dbus-x11 xserver-xorg-video-dummy; \
|
||||
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6; \
|
||||
#
|
||||
# gst
|
||||
apt-get install -y --no-install-recommends libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio; \
|
||||
# intel driver + vaapi
|
||||
apt-get install -y --no-install-recommends intel-media-va-driver-non-free libva2 vainfo; \
|
||||
#
|
||||
# fonts
|
||||
apt-get install -y --no-install-recommends fonts-takao-mincho; \
|
||||
# gst + vaapi plugin
|
||||
apt-get install -y --no-install-recommends libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio \
|
||||
gstreamer1.0-vaapi ;\
|
||||
#
|
||||
# install fonts
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Google emojis
|
||||
fonts-noto-color-emoji \
|
||||
# Japanese fonts
|
||||
fonts-takao-mincho \
|
||||
# Chinese fonts
|
||||
fonts-wqy-zenhei; \
|
||||
#
|
||||
# create a non-root user
|
||||
groupadd --gid $USER_GID $USERNAME; \
|
||||
@ -106,6 +122,7 @@ COPY .docker/base/dbus /usr/bin/dbus
|
||||
COPY .docker/base/default.pa /etc/pulse/default.pa
|
||||
COPY .docker/base/supervisord.conf /etc/neko/supervisord.conf
|
||||
COPY .docker/base/xorg.conf /etc/neko/xorg.conf
|
||||
COPY .docker/base/add-render-group.sh /usr/bin/add-render-group.sh
|
||||
|
||||
#
|
||||
# set default envs
|
||||
@ -114,6 +131,7 @@ ENV DISPLAY=:99.0
|
||||
ENV NEKO_PASSWORD=neko
|
||||
ENV NEKO_PASSWORD_ADMIN=admin
|
||||
ENV NEKO_BIND=:8080
|
||||
ENV RENDER_GID=
|
||||
|
||||
#
|
||||
# copy static files from previous stages
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# STAGE 1: SERVER
|
||||
#
|
||||
FROM arm32v7/golang:1.17-buster as server
|
||||
FROM arm32v7/golang:1.18-buster as server
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
@ -77,7 +77,7 @@ RUN set -eux; apt-get update; \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio gstreamer1.0-omx; \
|
||||
#
|
||||
# fonts
|
||||
apt-get install -y --no-install-recommends fonts-takao-mincho; \
|
||||
apt-get install -y --no-install-recommends fonts-takao-mincho fonts-wqy-zenhei; \
|
||||
#
|
||||
# create a non-root user
|
||||
groupadd --gid $USER_GID $USERNAME; \
|
||||
|
18
.docker/base/add-render-group.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# if no hwenc required, noop
|
||||
[[ -z "$NEKO_HWENC" ]] && exit 0
|
||||
|
||||
if [[ -z "$RENDER_GID" ]]; then
|
||||
RENDER_GID=$(stat -c "%g" /dev/dri/render* | tail -n 1)
|
||||
# is /dev/dri passed to the container?
|
||||
[[ -z "$RENDER_GID" ]] && exit 1
|
||||
fi
|
||||
|
||||
# note that this could conceivably be a security risk...
|
||||
cnt_group=$(getent group "$RENDER_GID" | cut -d: -f1)
|
||||
if [[ -z "$cnt_group" ]]; then
|
||||
groupadd -g "$RENDER_GID" nekorender
|
||||
cnt_group=nekorender
|
||||
fi
|
||||
usermod -a -G "$cnt_group" "$USER"
|
@ -9,6 +9,19 @@ loglevel=debug
|
||||
[include]
|
||||
files=/etc/neko/supervisord/*.conf
|
||||
|
||||
[program:rendergroup-init]
|
||||
environment=RENDER_GID="%(ENV_RENDER_GID)s",USER="%(ENV_USER)s"
|
||||
command=/usr/bin/add-render-group.sh
|
||||
startsecs=0
|
||||
startretries=0
|
||||
autorestart=false
|
||||
priority=10
|
||||
user=root
|
||||
stdout_logfile=/var/log/neko/rendergroup.log
|
||||
stdout_logfile_maxbytes=1MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
||||
|
||||
[program:dbus]
|
||||
environment=HOME="/root",USER="root"
|
||||
command=/usr/bin/dbus
|
||||
@ -33,7 +46,7 @@ redirect_stderr=true
|
||||
|
||||
[program:pulseaudio]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/pulseaudio --disallow-module-loading -vvvv --disallow-exit --exit-idle-time=-1
|
||||
command=/usr/bin/pulseaudio --log-level=info --disallow-module-loading --disallow-exit --exit-idle-time=-1
|
||||
autorestart=true
|
||||
priority=300
|
||||
user=%(ENV_USER)s
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Brave-browser*" name="brave-browser*">
|
||||
<application class="Brave-browser*" name="brave-browser*" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -1,6 +1,4 @@
|
||||
{
|
||||
"HomepageLocation": "",
|
||||
"AutoFillEnabled": false,
|
||||
"AutofillAddressEnabled": false,
|
||||
"AutofillCreditCardEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
@ -20,19 +18,22 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLBlacklist": [
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
],
|
||||
"ExtensionInstallForcelist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa;https://clients2.google.com/service/update2/crx"
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
|
||||
],
|
||||
"ExtensionInstallWhitelist": [
|
||||
"ExtensionInstallAllowlist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa"
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone"
|
||||
],
|
||||
"ExtensionInstallBlacklist": [
|
||||
"ExtensionInstallBlocklist": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -ex
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
BASE="${PWD}/../"
|
||||
|
@ -3,14 +3,21 @@ FROM $BASE_IMAGE
|
||||
|
||||
#
|
||||
# install neko chromium
|
||||
RUN set -eux; apt-get update; \
|
||||
apt-get install -y --no-install-recommends unzip chromium chromium-sandbox openbox; \
|
||||
RUN set -eux; \
|
||||
echo "deb http://ftp.de.debian.org/debian bookworm main" >> /etc/apt/sources.list; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends unzip chromium chromium-common chromium-sandbox openbox; \
|
||||
#
|
||||
# install widevine module
|
||||
CHROMIUM_DIR="/usr/lib/chromium"; \
|
||||
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
|
||||
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/$WIDEVINE_VERSION-linux-x64.zip"; \
|
||||
unzip -p /tmp/widevine.zip libwidevinecdm.so > /usr/lib/chromium/libwidevinecdm.so; \
|
||||
chmod 644 /usr/lib/chromium/libwidevinecdm.so; \
|
||||
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
|
||||
mkdir -p "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64"; \
|
||||
unzip -p /tmp/widevine.zip LICENSE.txt > "${CHROMIUM_DIR}/WidevineCdm/LICENSE"; \
|
||||
unzip -p /tmp/widevine.zip manifest.json > "${CHROMIUM_DIR}/WidevineCdm/manifest.json"; \
|
||||
unzip -p /tmp/widevine.zip libwidevinecdm.so > "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so"; \
|
||||
find "${CHROMIUM_DIR}/WidevineCdm" -type d -exec chmod 0755 '{}' \;; \
|
||||
find "${CHROMIUM_DIR}/WidevineCdm" -type f -exec chmod 0644 '{}' \;; \
|
||||
rm /tmp/widevine.zip; \
|
||||
#
|
||||
# clean up
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Chromium*" name="chromium*">
|
||||
<application class="Chromium*" name="chromium*" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -1,6 +1,4 @@
|
||||
{
|
||||
"HomepageLocation": "",
|
||||
"AutoFillEnabled": false,
|
||||
"AutofillAddressEnabled": false,
|
||||
"AutofillCreditCardEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
@ -20,19 +18,23 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLBlacklist": [
|
||||
"BrowserLabsEnabled": false,
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
],
|
||||
"ExtensionInstallForcelist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa;https://clients2.google.com/service/update2/crx"
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
|
||||
],
|
||||
"ExtensionInstallWhitelist": [
|
||||
"ExtensionInstallAllowlist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa"
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone"
|
||||
],
|
||||
"ExtensionInstallBlacklist": [
|
||||
"ExtensionInstallBlocklist": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ RUN set -eux; apt-get update; \
|
||||
# install extensions
|
||||
mkdir -p /usr/lib/firefox-esr/distribution/extensions; \
|
||||
wget -O '/usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi' https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi; \
|
||||
wget -O /usr/lib/firefox-esr/distribution/extensions/nordvpnproxy@nordvpn.com.xpi https://addons.mozilla.org/firefox/downloads/latest/nordvpn-proxy-extension/latest.xpi; \
|
||||
wget -O '/usr/lib/firefox-esr/distribution/extensions/sponsorBlocker@ajay.app.xpi' https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi; \
|
||||
#
|
||||
# create a profile directory
|
||||
mkdir -p /home/neko/.mozilla/firefox/profile.default/extensions; \
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Firefox*" name="Navigator">
|
||||
<application class="firefox" name="Navigator" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -56,8 +56,8 @@
|
||||
"*": {
|
||||
"installation_mode": "blocked"
|
||||
},
|
||||
"nordvpnproxy@nordvpn.com": {
|
||||
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/nordvpn-proxy-extension/latest.xpi",
|
||||
"sponsorBlocker@ajay.app": {
|
||||
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi",
|
||||
"installation_mode": "force_installed"
|
||||
},
|
||||
"uBlock0@raymondhill.net": {
|
||||
@ -108,7 +108,7 @@
|
||||
"datareporting.policy.dataSubmissionPolicyBypassNotification": true,
|
||||
"dom.disable_window_flip": true,
|
||||
"dom.disable_window_move_resize": true,
|
||||
"dom.event.contextmenu.enabled": false,
|
||||
"dom.event.contextmenu.enabled": true,
|
||||
"extensions.getAddons.showPane": false,
|
||||
"places.history.enabled": false,
|
||||
"privacy.file_unique_origin": true,
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Google-chrome*" name="google-chrome*">
|
||||
<application class="Google-chrome*" name="google-chrome*" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -1,6 +1,4 @@
|
||||
{
|
||||
"HomepageLocation": "",
|
||||
"AutoFillEnabled": false,
|
||||
"AutofillAddressEnabled": false,
|
||||
"AutofillCreditCardEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
@ -20,19 +18,22 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLBlacklist": [
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
],
|
||||
"ExtensionInstallForcelist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa;https://clients2.google.com/service/update2/crx"
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
|
||||
],
|
||||
"ExtensionInstallWhitelist": [
|
||||
"ExtensionInstallAllowlist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa"
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone"
|
||||
],
|
||||
"ExtensionInstallBlacklist": [
|
||||
"ExtensionInstallBlocklist": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
|
24
.docker/microsoft-edge/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
ARG API_URL="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/"
|
||||
|
||||
#
|
||||
# install microsoft edge
|
||||
RUN set -eux; apt-get update; \
|
||||
#
|
||||
# fetch latest release
|
||||
SRC_URL="${API_URL}$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"]*\).*/\1/p' | tail -1)"; \
|
||||
wget -O /tmp/microsoft-edge.deb "${SRC_URL}"; \
|
||||
apt-get install -y --no-install-recommends openbox /tmp/microsoft-edge.deb; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.conf /etc/neko/supervisord/microsoft-edge.conf
|
||||
COPY --chown=neko preferences.json /home/neko/.config/microsoft-edge/Default/Preferences
|
||||
COPY policies.json /etc/opt/edge/policies/managed/policies.json
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
763
.docker/microsoft-edge/openbox.xml
Normal file
@ -0,0 +1,763 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- Default openbox config but all window decorations are moved
|
||||
thereby making it harder to accidentally close the virtual browser -->
|
||||
|
||||
<openbox_config xmlns="http://openbox.org/3.4/rc"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
|
||||
<resistance>
|
||||
<strength>10</strength>
|
||||
<screen_edge_strength>20</screen_edge_strength>
|
||||
</resistance>
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Microsoft-edge*" name="microsoft-edge*" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
<layer>normal</layer>
|
||||
</application>
|
||||
</applications>
|
||||
|
||||
<focus>
|
||||
<focusNew>yes</focusNew>
|
||||
<!-- always try to focus new windows when they appear. other rules do
|
||||
apply -->
|
||||
<followMouse>no</followMouse>
|
||||
<!-- move focus to a window when you move the mouse into it -->
|
||||
<focusLast>yes</focusLast>
|
||||
<!-- focus the last used window when changing desktops, instead of the one
|
||||
under the mouse pointer. when followMouse is enabled -->
|
||||
<underMouse>no</underMouse>
|
||||
<!-- move focus under the mouse, even when the mouse is not moving -->
|
||||
<focusDelay>200</focusDelay>
|
||||
<!-- when followMouse is enabled, the mouse must be inside the window for
|
||||
this many milliseconds (1000 = 1 sec) before moving focus to it -->
|
||||
<raiseOnFocus>no</raiseOnFocus>
|
||||
<!-- when followMouse is enabled, and a window is given focus by moving the
|
||||
mouse into it, also raise the window -->
|
||||
</focus>
|
||||
|
||||
<placement>
|
||||
<policy>Smart</policy>
|
||||
<!-- 'Smart' or 'UnderMouse' -->
|
||||
<center>yes</center>
|
||||
<!-- whether to place windows in the center of the free area found or
|
||||
the top left corner -->
|
||||
<monitor>Primary</monitor>
|
||||
<!-- with Smart placement on a multi-monitor system, try to place new windows
|
||||
on: 'Any' - any monitor, 'Mouse' - where the mouse is, 'Active' - where
|
||||
the active window is, 'Primary' - only on the primary monitor -->
|
||||
<primaryMonitor>1</primaryMonitor>
|
||||
<!-- The monitor where Openbox should place popup dialogs such as the
|
||||
focus cycling popup, or the desktop switch popup. It can be an index
|
||||
from 1, specifying a particular monitor. Or it can be one of the
|
||||
following: 'Mouse' - where the mouse is, or
|
||||
'Active' - where the active window is -->
|
||||
</placement>
|
||||
|
||||
<theme>
|
||||
<name>Clearlooks</name>
|
||||
<titleLayout>NLIMC</titleLayout>
|
||||
<!--
|
||||
available characters are NDSLIMC, each can occur at most once.
|
||||
N: window icon
|
||||
L: window label (AKA title).
|
||||
I: iconify
|
||||
M: maximize
|
||||
C: close
|
||||
S: shade (roll up/down)
|
||||
D: omnipresent (on all desktops).
|
||||
-->
|
||||
<keepBorder>yes</keepBorder>
|
||||
<animateIconify>yes</animateIconify>
|
||||
<font place="ActiveWindow">
|
||||
<name>sans</name>
|
||||
<size>8</size>
|
||||
<!-- font size in points -->
|
||||
<weight>bold</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="InactiveWindow">
|
||||
<name>sans</name>
|
||||
<size>8</size>
|
||||
<!-- font size in points -->
|
||||
<weight>bold</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="MenuHeader">
|
||||
<name>sans</name>
|
||||
<size>9</size>
|
||||
<!-- font size in points -->
|
||||
<weight>normal</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="MenuItem">
|
||||
<name>sans</name>
|
||||
<size>9</size>
|
||||
<!-- font size in points -->
|
||||
<weight>normal</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="ActiveOnScreenDisplay">
|
||||
<name>sans</name>
|
||||
<size>9</size>
|
||||
<!-- font size in points -->
|
||||
<weight>bold</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="InactiveOnScreenDisplay">
|
||||
<name>sans</name>
|
||||
<size>9</size>
|
||||
<!-- font size in points -->
|
||||
<weight>bold</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
</theme>
|
||||
|
||||
<desktops>
|
||||
<!-- this stuff is only used at startup, pagers allow you to change them
|
||||
during a session
|
||||
|
||||
these are default values to use when other ones are not already set
|
||||
by other applications, or saved in your session
|
||||
|
||||
use obconf if you want to change these without having to log out
|
||||
and back in -->
|
||||
<number>1</number>
|
||||
<firstdesk>1</firstdesk>
|
||||
<names>
|
||||
<!-- set names up here if you want to, like this:
|
||||
<name>desktop 1</name>
|
||||
<name>desktop 2</name>
|
||||
-->
|
||||
</names>
|
||||
<popupTime>875</popupTime>
|
||||
<!-- The number of milliseconds to show the popup for when switching
|
||||
desktops. Set this to 0 to disable the popup. -->
|
||||
</desktops>
|
||||
|
||||
<resize>
|
||||
<drawContents>yes</drawContents>
|
||||
<popupShow>Nonpixel</popupShow>
|
||||
<!-- 'Always', 'Never', or 'Nonpixel' (xterms and such) -->
|
||||
<popupPosition>Center</popupPosition>
|
||||
<!-- 'Center', 'Top', or 'Fixed' -->
|
||||
<popupFixedPosition>
|
||||
<!-- these are used if popupPosition is set to 'Fixed' -->
|
||||
|
||||
<x>10</x>
|
||||
<!-- positive number for distance from left edge, negative number for
|
||||
distance from right edge, or 'Center' -->
|
||||
<y>10</y>
|
||||
<!-- positive number for distance from top edge, negative number for
|
||||
distance from bottom edge, or 'Center' -->
|
||||
</popupFixedPosition>
|
||||
</resize>
|
||||
|
||||
<!-- You can reserve a portion of your screen where windows will not cover when
|
||||
they are maximized, or when they are initially placed.
|
||||
Many programs reserve space automatically, but you can use this in other
|
||||
cases. -->
|
||||
<margins>
|
||||
<top>0</top>
|
||||
<bottom>0</bottom>
|
||||
<left>0</left>
|
||||
<right>0</right>
|
||||
</margins>
|
||||
|
||||
<dock>
|
||||
<position>TopLeft</position>
|
||||
<!-- (Top|Bottom)(Left|Right|)|Top|Bottom|Left|Right|Floating -->
|
||||
<floatingX>0</floatingX>
|
||||
<floatingY>0</floatingY>
|
||||
<noStrut>no</noStrut>
|
||||
<stacking>Above</stacking>
|
||||
<!-- 'Above', 'Normal', or 'Below' -->
|
||||
<direction>Vertical</direction>
|
||||
<!-- 'Vertical' or 'Horizontal' -->
|
||||
<autoHide>no</autoHide>
|
||||
<hideDelay>300</hideDelay>
|
||||
<!-- in milliseconds (1000 = 1 second) -->
|
||||
<showDelay>300</showDelay>
|
||||
<!-- in milliseconds (1000 = 1 second) -->
|
||||
<moveButton>Middle</moveButton>
|
||||
<!-- 'Left', 'Middle', 'Right' -->
|
||||
</dock>
|
||||
|
||||
<keyboard>
|
||||
<chainQuitKey>C-g</chainQuitKey>
|
||||
|
||||
<!-- Keybindings for desktop switching -->
|
||||
<keybind key="C-A-Left">
|
||||
<action name="GoToDesktop"><to>left</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="C-A-Right">
|
||||
<action name="GoToDesktop"><to>right</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="C-A-Up">
|
||||
<action name="GoToDesktop"><to>up</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="C-A-Down">
|
||||
<action name="GoToDesktop"><to>down</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="S-A-Left">
|
||||
<action name="SendToDesktop"><to>left</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="S-A-Right">
|
||||
<action name="SendToDesktop"><to>right</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="S-A-Up">
|
||||
<action name="SendToDesktop"><to>up</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="S-A-Down">
|
||||
<action name="SendToDesktop"><to>down</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="W-F1">
|
||||
<action name="GoToDesktop"><to>1</to></action>
|
||||
</keybind>
|
||||
<keybind key="W-F2">
|
||||
<action name="GoToDesktop"><to>2</to></action>
|
||||
</keybind>
|
||||
<keybind key="W-F3">
|
||||
<action name="GoToDesktop"><to>3</to></action>
|
||||
</keybind>
|
||||
<keybind key="W-F4">
|
||||
<action name="GoToDesktop"><to>4</to></action>
|
||||
</keybind>
|
||||
<keybind key="W-d">
|
||||
<action name="ToggleShowDesktop"/>
|
||||
</keybind>
|
||||
|
||||
<!-- Keybindings for windows -->
|
||||
<keybind key="A-F4">
|
||||
<action name="Close"/>
|
||||
</keybind>
|
||||
<keybind key="A-Escape">
|
||||
<action name="Lower"/>
|
||||
<action name="FocusToBottom"/>
|
||||
<action name="Unfocus"/>
|
||||
</keybind>
|
||||
<keybind key="A-space">
|
||||
<!--action name="ShowMenu"><menu>client-menu</menu></action-->
|
||||
</keybind>
|
||||
<!-- Take a screenshot of the current window with scrot when Alt+Print are pressed -->
|
||||
<keybind key="A-Print">
|
||||
<action name="Execute"><command>scrot -s</command></action>
|
||||
</keybind>
|
||||
|
||||
<!-- Keybindings for window switching -->
|
||||
<keybind key="A-Tab">
|
||||
<action name="NextWindow">
|
||||
<finalactions>
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</finalactions>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="A-S-Tab">
|
||||
<action name="PreviousWindow">
|
||||
<finalactions>
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</finalactions>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="C-A-Tab">
|
||||
<action name="NextWindow">
|
||||
<panels>yes</panels><desktop>yes</desktop>
|
||||
<finalactions>
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</finalactions>
|
||||
</action>
|
||||
</keybind>
|
||||
|
||||
<!-- Keybindings for window switching with the arrow keys -->
|
||||
<keybind key="W-S-Right">
|
||||
<action name="DirectionalCycleWindows">
|
||||
<direction>right</direction>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="W-S-Left">
|
||||
<action name="DirectionalCycleWindows">
|
||||
<direction>left</direction>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="W-S-Up">
|
||||
<action name="DirectionalCycleWindows">
|
||||
<direction>up</direction>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="W-S-Down">
|
||||
<action name="DirectionalCycleWindows">
|
||||
<direction>down</direction>
|
||||
</action>
|
||||
</keybind>
|
||||
|
||||
<!-- Keybindings for running applications -->
|
||||
<keybind key="W-e">
|
||||
<action name="Execute">
|
||||
<startupnotify>
|
||||
<enabled>true</enabled>
|
||||
<name>Konqueror</name>
|
||||
</startupnotify>
|
||||
<command>kfmclient openProfile filemanagement</command>
|
||||
</action>
|
||||
</keybind>
|
||||
<!-- Launch scrot when Print is pressed -->
|
||||
<keybind key="Print">
|
||||
<action name="Execute"><command>scrot</command></action>
|
||||
</keybind>
|
||||
</keyboard>
|
||||
|
||||
<mouse>
|
||||
<dragThreshold>1</dragThreshold>
|
||||
<!-- number of pixels the mouse must move before a drag begins -->
|
||||
<doubleClickTime>500</doubleClickTime>
|
||||
<!-- in milliseconds (1000 = 1 second) -->
|
||||
<screenEdgeWarpTime>400</screenEdgeWarpTime>
|
||||
<!-- Time before changing desktops when the pointer touches the edge of the
|
||||
screen while moving a window, in milliseconds (1000 = 1 second).
|
||||
Set this to 0 to disable warping -->
|
||||
<screenEdgeWarpMouse>false</screenEdgeWarpMouse>
|
||||
<!-- Set this to TRUE to move the mouse pointer across the desktop when
|
||||
switching due to hitting the edge of the screen -->
|
||||
|
||||
<context name="Frame">
|
||||
<mousebind button="A-Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="A-Left" action="Click">
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="A-Left" action="Drag">
|
||||
<action name="Move"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="A-Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="A-Right" action="Drag">
|
||||
<action name="Resize"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="A-Middle" action="Press">
|
||||
<action name="Lower"/>
|
||||
<action name="FocusToBottom"/>
|
||||
<action name="Unfocus"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="C-A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="C-A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-S-Up" action="Click">
|
||||
<action name="SendToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-S-Down" action="Click">
|
||||
<action name="SendToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Titlebar">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Move"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="DoubleClick">
|
||||
<action name="ToggleMaximize"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="Up" action="Click">
|
||||
<action name="if">
|
||||
<shaded>no</shaded>
|
||||
<then>
|
||||
<action name="Shade"/>
|
||||
<action name="FocusToBottom"/>
|
||||
<action name="Unfocus"/>
|
||||
<action name="Lower"/>
|
||||
</then>
|
||||
</action>
|
||||
</mousebind>
|
||||
<mousebind button="Down" action="Click">
|
||||
<action name="if">
|
||||
<shaded>yes</shaded>
|
||||
<then>
|
||||
<action name="Unshade"/>
|
||||
<action name="Raise"/>
|
||||
</then>
|
||||
</action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Titlebar Top Right Bottom Left TLCorner TRCorner BRCorner BLCorner">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="Middle" action="Press">
|
||||
<action name="Lower"/>
|
||||
<action name="FocusToBottom"/>
|
||||
<action name="Unfocus"/>
|
||||
</mousebind>
|
||||
|
||||
<!--mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||
</mousebind-->
|
||||
</context>
|
||||
|
||||
<context name="Top">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"><edge>top</edge></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Left">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"><edge>left</edge></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Right">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"><edge>right</edge></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Bottom">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"><edge>bottom</edge></action>
|
||||
</mousebind>
|
||||
|
||||
<!--mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||
</mousebind-->
|
||||
</context>
|
||||
|
||||
<context name="TRCorner BRCorner TLCorner BLCorner">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Client">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Middle" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Icon">
|
||||
<!--mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||
</mousebind-->
|
||||
</context>
|
||||
|
||||
<context name="AllDesktops">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="ToggleOmnipresent"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Shade">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="ToggleShade"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Iconify">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="Iconify"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Maximize">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Middle" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="ToggleMaximize"/>
|
||||
</mousebind>
|
||||
<mousebind button="Middle" action="Click">
|
||||
<action name="ToggleMaximize"><direction>vertical</direction></action>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Click">
|
||||
<action name="ToggleMaximize"><direction>horizontal</direction></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Close">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="Close"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Desktop">
|
||||
<mousebind button="Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="C-A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="C-A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Root">
|
||||
<!-- Menus -->
|
||||
<!--mousebind button="Middle" action="Press">
|
||||
<action name="ShowMenu"><menu>client-list-combined-menu</menu></action>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="ShowMenu"><menu>root-menu</menu></action>
|
||||
</mousebind-->
|
||||
</context>
|
||||
|
||||
<context name="MoveResize">
|
||||
<mousebind button="Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
</mouse>
|
||||
|
||||
<menu>
|
||||
<!-- You can specify more than one menu file in here and they are all loaded,
|
||||
just don't make menu ids clash or, well, it'll be kind of pointless -->
|
||||
|
||||
<!-- default menu file (or custom one in $HOME/.config/openbox/) -->
|
||||
<!-- system menu files on Debian systems -->
|
||||
<!--file>/var/lib/openbox/debian-menu.xml</file-->
|
||||
<file>menu.xml</file>
|
||||
<hideDelay>200</hideDelay>
|
||||
<!-- if a press-release lasts longer than this setting (in milliseconds), the
|
||||
menu is hidden again -->
|
||||
<middle>no</middle>
|
||||
<!-- center submenus vertically about the parent entry -->
|
||||
<submenuShowDelay>100</submenuShowDelay>
|
||||
<!-- time to delay before showing a submenu after hovering over the parent
|
||||
entry.
|
||||
if this is a negative value, then the delay is infinite and the
|
||||
submenu will not be shown until it is clicked on -->
|
||||
<submenuHideDelay>400</submenuHideDelay>
|
||||
<!-- time to delay before hiding a submenu when selecting another
|
||||
entry in parent menu
|
||||
if this is a negative value, then the delay is infinite and the
|
||||
submenu will not be hidden until a different submenu is opened -->
|
||||
<showIcons>yes</showIcons>
|
||||
<!-- controls if icons appear in the client-list-(combined-)menu -->
|
||||
<manageDesktops>yes</manageDesktops>
|
||||
<!-- show the manage desktops section in the client-list-(combined-)menu -->
|
||||
</menu>
|
||||
|
||||
<applications>
|
||||
<!--
|
||||
# this is an example with comments through out. use these to make your
|
||||
# own rules, but without the comments of course.
|
||||
# you may use one or more of the name/class/role/title/type rules to specify
|
||||
# windows to match
|
||||
|
||||
<application name="the window's _OB_APP_NAME property (see obxprop)"
|
||||
class="the window's _OB_APP_CLASS property (see obxprop)"
|
||||
groupname="the window's _OB_APP_GROUP_NAME property (see obxprop)"
|
||||
groupclass="the window's _OB_APP_GROUP_CLASS property (see obxprop)"
|
||||
role="the window's _OB_APP_ROLE property (see obxprop)"
|
||||
title="the window's _OB_APP_TITLE property (see obxprop)"
|
||||
type="the window's _OB_APP_TYPE property (see obxprob)..
|
||||
(if unspecified, then it is 'dialog' for child windows)">
|
||||
# you may set only one of name/class/role/title/type, or you may use more
|
||||
# than one together to restrict your matches.
|
||||
|
||||
# the name, class, role, and title use simple wildcard matching such as those
|
||||
# used by a shell. you can use * to match any characters and ? to match
|
||||
# any single character.
|
||||
|
||||
# the type is one of: normal, dialog, splash, utility, menu, toolbar, dock,
|
||||
# or desktop
|
||||
|
||||
# when multiple rules match a window, they will all be applied, in the
|
||||
# order that they appear in this list
|
||||
|
||||
|
||||
# each rule element can be left out or set to 'default' to specify to not
|
||||
# change that attribute of the window
|
||||
|
||||
<decor>yes</decor>
|
||||
# enable or disable window decorations
|
||||
|
||||
<shade>no</shade>
|
||||
# make the window shaded when it appears, or not
|
||||
|
||||
<position force="no">
|
||||
# the position is only used if both an x and y coordinate are provided
|
||||
# (and not set to 'default')
|
||||
# when force is "yes", then the window will be placed here even if it
|
||||
# says you want it placed elsewhere. this is to override buggy
|
||||
# applications who refuse to behave
|
||||
<x>center</x>
|
||||
# a number like 50, or 'center' to center on screen. use a negative number
|
||||
# to start from the right (or bottom for <y>), ie -50 is 50 pixels from
|
||||
# the right edge (or bottom). use 'default' to specify using value
|
||||
# provided by the application, or chosen by openbox, instead.
|
||||
<y>200</y>
|
||||
<monitor>1</monitor>
|
||||
# specifies the monitor in a xinerama setup.
|
||||
# 1 is the first head, or 'mouse' for wherever the mouse is
|
||||
</position>
|
||||
|
||||
<size>
|
||||
# the size to make the window.
|
||||
<width>20</width>
|
||||
# a number like 20, or 'default' to use the size given by the application.
|
||||
# you can use fractions such as 1/2 or percentages such as 75% in which
|
||||
# case the value is relative to the size of the monitor that the window
|
||||
# appears on.
|
||||
<height>30%</height>
|
||||
</size>
|
||||
|
||||
<focus>yes</focus>
|
||||
# if the window should try be given focus when it appears. if this is set
|
||||
# to yes it doesn't guarantee the window will be given focus. some
|
||||
# restrictions may apply, but Openbox will try to
|
||||
|
||||
<desktop>1</desktop>
|
||||
# 1 is the first desktop, 'all' for all desktops
|
||||
|
||||
<layer>normal</layer>
|
||||
# 'above', 'normal', or 'below'
|
||||
|
||||
<iconic>no</iconic>
|
||||
# make the window iconified when it appears, or not
|
||||
|
||||
<skip_pager>no</skip_pager>
|
||||
# asks to not be shown in pagers
|
||||
|
||||
<skip_taskbar>no</skip_taskbar>
|
||||
# asks to not be shown in taskbars. window cycling actions will also
|
||||
# skip past such windows
|
||||
|
||||
<fullscreen>yes</fullscreen>
|
||||
# make the window in fullscreen mode when it appears
|
||||
|
||||
<maximized>true</maximized>
|
||||
# 'Horizontal', 'Vertical' or boolean (yes/no)
|
||||
</application>
|
||||
|
||||
# end of the example
|
||||
-->
|
||||
</applications>
|
||||
|
||||
</openbox_config>
|
39
.docker/microsoft-edge/policies.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"AutofillAddressEnabled": false,
|
||||
"AutofillCreditCardEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
"DefaultNotificationsSetting": 2,
|
||||
"DeveloperToolsAvailability": 2,
|
||||
"EditBookmarksEnabled": false,
|
||||
"FullscreenAllowed": true,
|
||||
"IncognitoModeAvailability": 1,
|
||||
"SyncDisabled": true,
|
||||
"AutoplayAllowed": true,
|
||||
"BrowserAddPersonEnabled": false,
|
||||
"BrowserGuestModeEnabled": false,
|
||||
"DefaultPopupsSetting": 2,
|
||||
"DownloadRestrictions": 3,
|
||||
"VideoCaptureAllowed": true,
|
||||
"AllowFileSelectionDialogs": false,
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"edge://policy"
|
||||
],
|
||||
"ExtensionInstallForcelist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
|
||||
],
|
||||
"ExtensionInstallAllowlist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone"
|
||||
],
|
||||
"ExtensionInstallBlocklist": [
|
||||
"*"
|
||||
]
|
||||
}
|
110
.docker/microsoft-edge/preferences.json
Normal file
@ -0,0 +1,110 @@
|
||||
{
|
||||
"homepage": "http://www.google.com",
|
||||
"homepage_is_newtabpage": false,
|
||||
"first_run_tabs": [
|
||||
"https://www.google.com/_/chrome/newtab?ie=UTF-8"
|
||||
],
|
||||
"custom_links": {
|
||||
"initialized": true,
|
||||
"list": [
|
||||
{
|
||||
"title": "YouTube",
|
||||
"url": "https://www.youtube.com/"
|
||||
},
|
||||
{
|
||||
"title": "Netflix",
|
||||
"url": "https://netflix.com"
|
||||
},
|
||||
{
|
||||
"title": "Hulu",
|
||||
"url": "https://www.hulu.com/"
|
||||
},
|
||||
{
|
||||
"title": "9Anime",
|
||||
"url": "https://9anime.to/"
|
||||
},
|
||||
{
|
||||
"title": "Crunchy Roll",
|
||||
"url": "https://www.crunchyroll.com/"
|
||||
},
|
||||
{
|
||||
"title": "Funimation",
|
||||
"url": "https://www.funimation.com/"
|
||||
},
|
||||
{
|
||||
"title": "Disney+",
|
||||
"url": "https://www.disneyplus.com/"
|
||||
},
|
||||
{
|
||||
"title": "HBO Now",
|
||||
"url": "https://play.hbonow.com/"
|
||||
},
|
||||
{
|
||||
"title": "Amazon Video",
|
||||
"url": "https://www.amazon.com/Amazon-Video/b?node=2858778011"
|
||||
},
|
||||
{
|
||||
"title": "VRV",
|
||||
"url": "https://vrv.co/"
|
||||
},
|
||||
{
|
||||
"title": "Twitch",
|
||||
"url": "https://www.twitch.tv/"
|
||||
},
|
||||
{
|
||||
"title": "Mixer",
|
||||
"url": "https://mixer.com/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"browser": {
|
||||
"custom_chrome_frame": false,
|
||||
"show_home_button": true,
|
||||
"window_placement": {
|
||||
"maximized": true
|
||||
}
|
||||
},
|
||||
"bookmark_bar": {
|
||||
"show_on_all_tabs": false
|
||||
},
|
||||
"sync_promo": {
|
||||
"show_on_first_run_allowed": false
|
||||
},
|
||||
"distribution": {
|
||||
"import_bookmarks_from_file": "bookmarks.html",
|
||||
"import_bookmarks": true,
|
||||
"import_history": true,
|
||||
"import_home_page": true,
|
||||
"import_search_engine": true,
|
||||
"ping_delay": 60,
|
||||
"do_not_create_desktop_shortcut": true,
|
||||
"do_not_create_quick_launch_shortcut": true,
|
||||
"do_not_create_taskbar_shortcut": true,
|
||||
"do_not_launch_chrome": true,
|
||||
"do_not_register_for_update_launch": true,
|
||||
"make_chrome_default": true,
|
||||
"make_chrome_default_for_user": true,
|
||||
"system_level": false,
|
||||
"verbose_logging": false
|
||||
},
|
||||
"profile": {
|
||||
"avatar_index": 19,
|
||||
"default_content_setting_values": {
|
||||
"clipboard": 2,
|
||||
"cookies": 4,
|
||||
"geolocation": 2,
|
||||
"media_stream_camera": 2,
|
||||
"media_stream_mic": 2,
|
||||
"midi_sysex": 2,
|
||||
"payment_handler": 2,
|
||||
"usb_guard": 2
|
||||
},
|
||||
"name": "neko",
|
||||
"using_default_avatar": false,
|
||||
"using_default_name": false,
|
||||
"using_gaia_avatar": false
|
||||
},
|
||||
"signin": {
|
||||
"allowed": false
|
||||
}
|
||||
}
|
22
.docker/microsoft-edge/supervisord.conf
Normal file
@ -0,0 +1,22 @@
|
||||
[program:microsoft-edge]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/microsoft-edge --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/microsoft-edge --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=800
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/microsoft-edge.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
||||
|
||||
[program:openbox]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
|
||||
autorestart=true
|
||||
priority=300
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/openbox.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
31
.docker/opera/Dockerfile
Normal file
@ -0,0 +1,31 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
ARG API_URL="https://download5.operacdn.com/pub/opera/desktop/"
|
||||
ARG LIBFFMPEG_API_URL="https://api.github.com/repos/nwjs-ffmpeg-prebuilt/nwjs-ffmpeg-prebuilt/releases/latest"
|
||||
|
||||
#
|
||||
# install opera
|
||||
RUN set -eux; apt-get update; \
|
||||
#
|
||||
# fetch latest release
|
||||
VERSION="$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"/]*\).*/\1/p' | tail -1)"; \
|
||||
wget -O /tmp/opera.deb "${API_URL}${VERSION}/linux/opera-stable_${VERSION}_amd64.deb"; \
|
||||
apt-get install -y --no-install-recommends openbox jq unzip /tmp/opera.deb; \
|
||||
#
|
||||
# install libffmpeg
|
||||
LIBFFMPEG_URL="$(wget -O - "${LIBFFMPEG_API_URL}" 2>/dev/null | jq -r "[.assets[] | select(.browser_download_url | contains(\"linux-x64.zip\"))][-1] | .browser_download_url")"; \
|
||||
wget -O /tmp/libffmpeg.zip $LIBFFMPEG_URL; \
|
||||
unzip -o /tmp/libffmpeg.zip libffmpeg.so -d /usr/lib/x86_64-linux-gnu/opera/lib_extra; \
|
||||
echo '[]' > /usr/lib/x86_64-linux-gnu/opera/resources/ffmpeg_preload_config.json; \
|
||||
#
|
||||
# clean up
|
||||
apt-get --purge autoremove -y unzip jq; \
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.conf /etc/neko/supervisord/opera.conf
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Vncviewer" name="vncviewer">
|
||||
<application class="Opera" name="Opera" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
@ -1,11 +1,11 @@
|
||||
[program:vncviewer]
|
||||
[program:opera]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/vncviewer -autopass -x11cursor -nojpeg -quality 9 -compresslevel 9 %(ENV_NEKO_VNC_URL)s
|
||||
command=/usr/bin/opera --no-sandbox --no-first-run --start-maximized --force-dark-mode --disable-gpu
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=800
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/vncviewer.log
|
||||
stdout_logfile=/var/log/neko/opera.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
20
.docker/remmina/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
# install remmina
|
||||
RUN set -eux; apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
remmina-plugin-rdp remmina-plugin-vnc \
|
||||
# remmina-plugin-x2go # not in bullseye
|
||||
remmina-plugin-spice remmina-plugin-nx; \
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
# copy configuation files
|
||||
COPY supervisord.conf /etc/neko/supervisord/remmina.conf
|
||||
COPY --chown=neko remmina.pref /home/neko/.config/remmina/remmina.pref
|
||||
COPY --chown=neko rdp.remmina spice.remmina vnc.remmina /home/neko/.local/share/remmina/
|
||||
COPY run-remmina.sh /usr/bin/run-remmina.sh
|
||||
ENV REMMINA_URL=
|
||||
ENV REMMINA_PROFILE=
|
90
.docker/remmina/rdp.remmina
Normal file
@ -0,0 +1,90 @@
|
||||
[remmina]
|
||||
password=
|
||||
gateway_username=
|
||||
notes_text=
|
||||
vc=
|
||||
preferipv6=0
|
||||
ssh_tunnel_loopback=0
|
||||
serialname=
|
||||
sound=local
|
||||
printer_overrides=
|
||||
name=rdpdefault
|
||||
console=0
|
||||
colordepth=99
|
||||
security=
|
||||
precommand=
|
||||
disable_fastpath=0
|
||||
left-handed=0
|
||||
postcommand=
|
||||
multitransport=0
|
||||
group=
|
||||
server=
|
||||
ssh_tunnel_certfile=
|
||||
glyph-cache=0
|
||||
ssh_tunnel_enabled=0
|
||||
disableclipboard=0
|
||||
audio-output=
|
||||
parallelpath=
|
||||
monitorids=
|
||||
cert_ignore=0
|
||||
gateway_server=
|
||||
serialpermissive=0
|
||||
protocol=RDP
|
||||
old-license=0
|
||||
ssh_tunnel_password=
|
||||
resolution_mode=2
|
||||
pth=
|
||||
loadbalanceinfo=
|
||||
disableautoreconnect=0
|
||||
clientname=
|
||||
clientbuild=
|
||||
resolution_width=0
|
||||
drive=
|
||||
relax-order-checks=0
|
||||
username=
|
||||
base-cred-for-gw=0
|
||||
gateway_domain=
|
||||
network=none
|
||||
rdp2tcp=
|
||||
gateway_password=
|
||||
serialdriver=
|
||||
domain=
|
||||
profile-lock=0
|
||||
rdp_reconnect_attempts=
|
||||
restricted-admin=0
|
||||
multimon=0
|
||||
exec=
|
||||
smartcardname=
|
||||
serialpath=
|
||||
enable-autostart=0
|
||||
usb=
|
||||
shareprinter=0
|
||||
ssh_tunnel_passphrase=
|
||||
shareparallel=0
|
||||
disablepasswordstoring=0
|
||||
quality=0
|
||||
span=0
|
||||
parallelname=
|
||||
ssh_tunnel_auth=0
|
||||
keymap=
|
||||
ssh_tunnel_username=
|
||||
execpath=
|
||||
shareserial=0
|
||||
resolution_height=0
|
||||
timeout=
|
||||
useproxyenv=0
|
||||
sharesmartcard=0
|
||||
freerdp_log_filters=
|
||||
microphone=
|
||||
dvc=
|
||||
ssh_tunnel_privatekey=
|
||||
gwtransp=http
|
||||
ssh_tunnel_server=
|
||||
ignore-tls-errors=1
|
||||
disable-smooth-scrolling=0
|
||||
gateway_usage=0
|
||||
websockets=0
|
||||
freerdp_log_level=INFO
|
||||
window_maximize=1
|
||||
scale=1
|
||||
viewmode=4
|
103
.docker/remmina/remmina.pref
Normal file
@ -0,0 +1,103 @@
|
||||
[remmina_pref]
|
||||
secret=Jsh7BJPwHqLCKi2vdkMAImSgdBVZGF6s8VbUPY3Q9WA=
|
||||
datadir_path=
|
||||
remmina_file_name=%G_%P_%N_%h
|
||||
screenshot_path=
|
||||
screenshot_name=
|
||||
deny_screenshot_clipboard=true
|
||||
save_view_mode=true
|
||||
use_master_password=false
|
||||
unlock_timeout=0
|
||||
unlock_password=
|
||||
trust_all=true
|
||||
floating_toolbar_placement=0
|
||||
toolbar_placement=3
|
||||
prevent_snap_welcome_message=true
|
||||
last_quickconnect_protocol=
|
||||
fullscreen_on_auto=true
|
||||
always_show_tab=false
|
||||
hide_connection_toolbar=true
|
||||
hide_searchbar=true
|
||||
default_action=0
|
||||
scale_quality=3
|
||||
ssh_loglevel=1
|
||||
ssh_parseconfig=true
|
||||
hide_toolbar=true
|
||||
small_toolbutton=true
|
||||
view_file_mode=0
|
||||
resolutions=640x480,800x600,1024x768,1152x864,1280x960,1400x1050
|
||||
keystrokes=
|
||||
main_width=600
|
||||
main_height=400
|
||||
main_maximize=true
|
||||
main_sort_column_id=1
|
||||
main_sort_order=0
|
||||
expanded_group=
|
||||
toolbar_pin_down=false
|
||||
sshtunnel_port=4732
|
||||
ssh_tcp_keepidle=20
|
||||
ssh_tcp_keepintvl=10
|
||||
ssh_tcp_keepcnt=3
|
||||
ssh_tcp_usrtimeout=60000
|
||||
applet_new_ontop=false
|
||||
applet_hide_count=true
|
||||
applet_enable_avahi=false
|
||||
disable_tray_icon=true
|
||||
dark_tray_icon=false
|
||||
recent_maximum=10
|
||||
default_mode=3
|
||||
tab_mode=0
|
||||
fullscreen_toolbar_visibility=2
|
||||
auto_scroll_step=10
|
||||
hostkey=65508
|
||||
shortcutkey_fullscreen=
|
||||
shortcutkey_autofit=
|
||||
shortcutkey_nexttab=
|
||||
shortcutkey_prevtab=
|
||||
shortcutkey_scale=
|
||||
shortcutkey_grab=65508
|
||||
shortcutkey_viewonly=109
|
||||
shortcutkey_screenshot=
|
||||
shortcutkey_minimize=65478
|
||||
shortcutkey_disconnect=65473
|
||||
shortcutkey_toolbar=116
|
||||
vte_font=
|
||||
vte_allow_bold_text=true
|
||||
vte_lines=512
|
||||
|
||||
[ssh_colors]
|
||||
background=#d5ccba
|
||||
cursor=#45373c
|
||||
bold=#45373c
|
||||
foreground=#45373c
|
||||
color0=#20111b
|
||||
color1=#be100e
|
||||
color2=#858162
|
||||
color3=#eaa549
|
||||
color4=#426a79
|
||||
color5=#97522c
|
||||
color6=#989a9c
|
||||
color7=#968c83
|
||||
color8=#5e5252
|
||||
color9=#be100e
|
||||
color10=#858162
|
||||
color11=#eaa549
|
||||
color12=#426a79
|
||||
color13=#97522c
|
||||
color14=#989a9c
|
||||
color15=#d5ccba
|
||||
|
||||
[usage_stats]
|
||||
periodic_usage_stats_permitted=false
|
||||
periodic_usage_stats_last_sent=0
|
||||
periodic_usage_stats_uuid_prefix=
|
||||
|
||||
[remmina_news]
|
||||
periodic_news_permitted=false
|
||||
periodic_rmnews_last_get=0
|
||||
periodic_rmnews_get_count=1
|
||||
periodic_rmnews_uuid_prefix=
|
||||
|
||||
[remmina]
|
||||
name=
|
||||
ignore-tls-errors=1
|
38
.docker/remmina/run-remmina.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
|
||||
err() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
profile_dir="/home/neko/.local/share/remmina"
|
||||
if [[ -n "$REMMINA_PROFILE" ]]; then
|
||||
profile=${REMMINA_PROFILE%.remmina}.remmina
|
||||
file=${profile##/*/}
|
||||
[[ "$file" = "$profile" ]] && profile="$profile_dir"/"$file"
|
||||
[[ -f "$profile" ]] || err "Connection profile $profile not found"
|
||||
echo "Running remmina with connection profile $profile"
|
||||
exec remmina -c "$profile"
|
||||
fi
|
||||
|
||||
[[ -z "$REMMINA_URL" ]] && err "Neither 'REMMINA_PROFILE' nor 'REMMINA_URL' found in env vars"
|
||||
|
||||
readarray -t arr < <( echo -n "$REMMINA_URL" | perl -pe 's|^(\w+)\:\/\/(?:([^:]+)(?::([^@]+))?@)?(.*)$|\1\n\2\n\3\n\4|' )
|
||||
proto="${arr[0]}"
|
||||
user="${arr[1]}"
|
||||
pw="${arr[2]}"
|
||||
host="${arr[3]}"
|
||||
echo "Parsed url in 'REMMINA_URL': proto:$proto username:$user host:$host"
|
||||
|
||||
[[ "$proto" != "vnc" && "$proto" != "rdp" && "$proto" != "spice" ]] && err "Unsupported protocol $proto in connection url 'REMMINA_URL'"
|
||||
|
||||
profile="$profile_dir"/"$proto".remmina
|
||||
remmina --set-option username="$user" --update-profile "$profile"
|
||||
remmina --set-option password="$pw" --update-profile "$profile"
|
||||
remmina --set-option server="$host" --update-profile "$profile"
|
||||
|
||||
# remmina --set-option window_maximize=1 --update-profile "$profile"
|
||||
# remmina --set-option scale=1 --update-profile "$profile"
|
||||
|
||||
exec remmina -c "$profile"
|
33
.docker/remmina/spice.remmina
Normal file
@ -0,0 +1,33 @@
|
||||
[remmina]
|
||||
disablegstvideooverlay=0
|
||||
disablepasswordstoring=0
|
||||
sharesmartcard=0
|
||||
videocodec=0
|
||||
ssh_tunnel_password=
|
||||
postcommand=
|
||||
server=
|
||||
name=spicedefault
|
||||
ssh_tunnel_enabled=0
|
||||
profile-lock=0
|
||||
enable-autostart=0
|
||||
imagecompression=0
|
||||
password=
|
||||
precommand=
|
||||
disableclipboard=0
|
||||
group=
|
||||
ssh_tunnel_certfile=
|
||||
protocol=SPICE
|
||||
enableaudio=1
|
||||
viewonly=0
|
||||
ssh_tunnel_server=
|
||||
ssh_tunnel_loopback=0
|
||||
ssh_tunnel_auth=0
|
||||
ignore-tls-errors=1
|
||||
ssh_tunnel_username=
|
||||
ssh_tunnel_passphrase=
|
||||
ssh_tunnel_privatekey=
|
||||
notes_text=
|
||||
usetls=0
|
||||
window_maximize=1
|
||||
scale=1
|
||||
viewmode=4
|
12
.docker/remmina/supervisord.conf
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
[program:remmina]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/run-remmina.sh
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=500
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/remmina.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
49
.docker/remmina/vnc.remmina
Normal file
@ -0,0 +1,49 @@
|
||||
[remmina]
|
||||
encodings=
|
||||
videocodec=0
|
||||
name=temp eos
|
||||
ssh_tunnel_server=
|
||||
ssh_tunnel_privatekey=
|
||||
password=
|
||||
quality=0
|
||||
disablesmoothscrolling=0
|
||||
enableaudio=1
|
||||
precommand=
|
||||
disablegstvideooverlay=0
|
||||
ssh_tunnel_enabled=0
|
||||
sharesmartcard=0
|
||||
imagecompression=0
|
||||
ssh_tunnel_passphrase=
|
||||
ssh_tunnel_password=
|
||||
usetls=0
|
||||
viewonly=0
|
||||
disableserverinput=0
|
||||
depth_profile=0
|
||||
postcommand=
|
||||
tightencoding=0
|
||||
disablepasswordstoring=0
|
||||
lossy_encoding=0
|
||||
ignore-tls-errors=1
|
||||
gvncdebug=0
|
||||
ssh_tunnel_username=
|
||||
server=
|
||||
disableclipboard=0
|
||||
disableserverbell=0
|
||||
profile-lock=0
|
||||
enable-autostart=0
|
||||
window_maximize=1
|
||||
scale=1
|
||||
disableencryption=0
|
||||
ssh_tunnel_auth=0
|
||||
group=
|
||||
ssh_tunnel_loopback=0
|
||||
showcursor=1
|
||||
viewmode=4
|
||||
notes_text=
|
||||
keymap=
|
||||
colordepth=32
|
||||
proxy=
|
||||
protocol=VNC
|
||||
ssh_tunnel_certfile=
|
||||
shared=0
|
||||
username=
|
@ -5,6 +5,7 @@ ARG API_URL="https://api.github.com/repos/macchrome/linchrome/releases/latest"
|
||||
|
||||
#
|
||||
# install custom chromium build from woolyss with support for hevc/x265
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
RUN set -eux; apt-get update; \
|
||||
apt-get install -y --no-install-recommends wget unzip libatk1.0-0 libatk-bridge2.0-0 libatomic1 \
|
||||
libcups2 libgtk-3-0 libnss3 libpci3 libxcomposite1 libxss1 openbox xz-utils jq; \
|
||||
@ -21,11 +22,27 @@ RUN set -eux; apt-get update; \
|
||||
#
|
||||
# install widevine module
|
||||
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
|
||||
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/$WIDEVINE_VERSION-linux-x64.zip"; \
|
||||
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
|
||||
unzip -p /tmp/widevine.zip libwidevinecdm.so > /usr/lib/chromium/libwidevinecdm.so; \
|
||||
chmod 644 /usr/lib/chromium/libwidevinecdm.so; \
|
||||
rm /tmp/widevine.zip; \
|
||||
#
|
||||
# install latest version of uBlock Origin and SponsorBlock for YouTube
|
||||
CHROMIUM_VERSION="$(wget -O - "${API_URL}" 2>/dev/null | jq -r ".tag_name" | sed -e 's/v//' -e 's/-.*//')"; \
|
||||
EXTENSIONS_DIR="/usr/share/chromium/extensions"; \
|
||||
EXTENSIONS=( \
|
||||
cjpalhdlnbpafiamejdnhcphjbkeiagm \
|
||||
mnjggcdmjocbbbhaepdhchncahnbgone \
|
||||
); \
|
||||
mkdir -p "${EXTENSIONS_DIR}"; \
|
||||
for EXT_ID in "${EXTENSIONS[@]}"; \
|
||||
do \
|
||||
EXT_URL="https://clients2.google.com/service/update2/crx?response=redirect&nacl_arch=x86-64&prodversion=${CHROMIUM_VERSION}&acceptformat=crx2,crx3&x=id%3D${EXT_ID}%26installsource%3Dondemand%26uc"; \
|
||||
EXT_PATH="${EXTENSIONS_DIR}/${EXT_ID}.crx"; \
|
||||
wget -O "${EXT_PATH}" "${EXT_URL}"; \
|
||||
EXT_VERSION="$(unzip -p "${EXT_PATH}" manifest.json 2>/dev/null | jq -r ".version")"; \
|
||||
echo -e "{\n \"external_crx\": \"${EXT_PATH}\",\n \"external_version\": \"${EXT_VERSION}\"\n}" > "${EXTENSIONS_DIR}"/"${EXT_ID}".json; \
|
||||
done; \
|
||||
# clean up
|
||||
apt-get --purge autoremove -y xz-utils jq; \
|
||||
apt-get clean -y; \
|
||||
@ -37,7 +54,3 @@ COPY supervisord.conf /etc/neko/supervisord/ungoogled-chromium.conf
|
||||
COPY preferences.json /usr/lib/chromium/master_preferences
|
||||
COPY policies.json /etc/chromium/policies/managed/policies.json
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
||||
|
||||
#
|
||||
# copy extensions and policy files
|
||||
COPY extensions /usr/share/chromium/extensions
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"external_crx": "/usr/share/chromium/extensions/cjpalhdlnbpafiamejdnhcphjbkeiagm.crx",
|
||||
"external_version": "1.38.6"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"external_crx": "/usr/share/chromium/extensions/fjoaledfpmneenckfbpdfhkmimnjocfa.crx",
|
||||
"external_version": "2.32.0"
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Chromium*" name="chromium-devel">
|
||||
<application class="Chromium*" name="chromium-devel" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -1,6 +1,4 @@
|
||||
{
|
||||
"HomepageLocation": "",
|
||||
"AutoFillEnabled": false,
|
||||
"AutofillAddressEnabled": false,
|
||||
"AutofillCreditCardEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
@ -20,15 +18,18 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLBlacklist": [
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
],
|
||||
"ExtensionInstallWhitelist": [
|
||||
"ExtensionInstallAllowlist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa"
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone"
|
||||
],
|
||||
"ExtensionInstallBlacklist": [
|
||||
"ExtensionInstallBlocklist": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
|
42
.docker/vivaldi/Dockerfile
Normal file
@ -0,0 +1,42 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
ARG VIVALDI_VERSION="5.3.2679.34-1"
|
||||
# TODO: Get chromium version from vivaldi
|
||||
ARG CHROMIUM_VERSION="102.0.5005.72"
|
||||
|
||||
#
|
||||
# install vivaldi
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
RUN set -eux; apt-get update; \
|
||||
wget -O /tmp/vivaldi.deb "https://downloads.vivaldi.com/stable/vivaldi-stable_${VIVALDI_VERSION}_amd64.deb"; \
|
||||
apt-get install -y --no-install-recommends wget unzip xz-utils jq openbox /tmp/vivaldi.deb; \
|
||||
/opt/vivaldi/update-ffmpeg; \
|
||||
#
|
||||
# install latest version of uBlock Origin and SponsorBlock for YouTube
|
||||
EXTENSIONS_DIR="/usr/share/chromium/extensions"; \
|
||||
EXTENSIONS=( \
|
||||
cjpalhdlnbpafiamejdnhcphjbkeiagm \
|
||||
mnjggcdmjocbbbhaepdhchncahnbgone \
|
||||
); \
|
||||
mkdir -p "${EXTENSIONS_DIR}"; \
|
||||
for EXT_ID in "${EXTENSIONS[@]}"; \
|
||||
do \
|
||||
EXT_URL="https://clients2.google.com/service/update2/crx?response=redirect&nacl_arch=x86-64&prodversion=${CHROMIUM_VERSION}&acceptformat=crx2,crx3&x=id%3D${EXT_ID}%26installsource%3Dondemand%26uc"; \
|
||||
EXT_PATH="${EXTENSIONS_DIR}/${EXT_ID}.crx"; \
|
||||
wget -O "${EXT_PATH}" "${EXT_URL}"; \
|
||||
EXT_VERSION="$(unzip -p "${EXT_PATH}" manifest.json 2>/dev/null | jq -r ".version")"; \
|
||||
echo -e "{\n \"external_crx\": \"${EXT_PATH}\",\n \"external_version\": \"${EXT_VERSION}\"\n}" > "${EXTENSIONS_DIR}"/"${EXT_ID}".json; \
|
||||
done; \
|
||||
#
|
||||
# clean up
|
||||
apt-get --purge autoremove -y xz-utils jq; \
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.conf /etc/neko/supervisord/vivaldi-browser.conf
|
||||
COPY --chown=neko preferences.json /home/neko/.config/vivaldi/Default/Preferences
|
||||
COPY policies.json /etc/opt/vivaldi/policies/managed/policies.json
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
763
.docker/vivaldi/openbox.xml
Normal file
@ -0,0 +1,763 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- Default openbox config but all window decorations are moved
|
||||
thereby making it harder to accidentally close the virtual browser -->
|
||||
|
||||
<openbox_config xmlns="http://openbox.org/3.4/rc"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
|
||||
<resistance>
|
||||
<strength>10</strength>
|
||||
<screen_edge_strength>20</screen_edge_strength>
|
||||
</resistance>
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Vivaldi-stable*" name="vivaldi-stable*" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
<layer>normal</layer>
|
||||
</application>
|
||||
</applications>
|
||||
|
||||
<focus>
|
||||
<focusNew>yes</focusNew>
|
||||
<!-- always try to focus new windows when they appear. other rules do
|
||||
apply -->
|
||||
<followMouse>no</followMouse>
|
||||
<!-- move focus to a window when you move the mouse into it -->
|
||||
<focusLast>yes</focusLast>
|
||||
<!-- focus the last used window when changing desktops, instead of the one
|
||||
under the mouse pointer. when followMouse is enabled -->
|
||||
<underMouse>no</underMouse>
|
||||
<!-- move focus under the mouse, even when the mouse is not moving -->
|
||||
<focusDelay>200</focusDelay>
|
||||
<!-- when followMouse is enabled, the mouse must be inside the window for
|
||||
this many milliseconds (1000 = 1 sec) before moving focus to it -->
|
||||
<raiseOnFocus>no</raiseOnFocus>
|
||||
<!-- when followMouse is enabled, and a window is given focus by moving the
|
||||
mouse into it, also raise the window -->
|
||||
</focus>
|
||||
|
||||
<placement>
|
||||
<policy>Smart</policy>
|
||||
<!-- 'Smart' or 'UnderMouse' -->
|
||||
<center>yes</center>
|
||||
<!-- whether to place windows in the center of the free area found or
|
||||
the top left corner -->
|
||||
<monitor>Primary</monitor>
|
||||
<!-- with Smart placement on a multi-monitor system, try to place new windows
|
||||
on: 'Any' - any monitor, 'Mouse' - where the mouse is, 'Active' - where
|
||||
the active window is, 'Primary' - only on the primary monitor -->
|
||||
<primaryMonitor>1</primaryMonitor>
|
||||
<!-- The monitor where Openbox should place popup dialogs such as the
|
||||
focus cycling popup, or the desktop switch popup. It can be an index
|
||||
from 1, specifying a particular monitor. Or it can be one of the
|
||||
following: 'Mouse' - where the mouse is, or
|
||||
'Active' - where the active window is -->
|
||||
</placement>
|
||||
|
||||
<theme>
|
||||
<name>Clearlooks</name>
|
||||
<titleLayout>NLIMC</titleLayout>
|
||||
<!--
|
||||
available characters are NDSLIMC, each can occur at most once.
|
||||
N: window icon
|
||||
L: window label (AKA title).
|
||||
I: iconify
|
||||
M: maximize
|
||||
C: close
|
||||
S: shade (roll up/down)
|
||||
D: omnipresent (on all desktops).
|
||||
-->
|
||||
<keepBorder>yes</keepBorder>
|
||||
<animateIconify>yes</animateIconify>
|
||||
<font place="ActiveWindow">
|
||||
<name>sans</name>
|
||||
<size>8</size>
|
||||
<!-- font size in points -->
|
||||
<weight>bold</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="InactiveWindow">
|
||||
<name>sans</name>
|
||||
<size>8</size>
|
||||
<!-- font size in points -->
|
||||
<weight>bold</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="MenuHeader">
|
||||
<name>sans</name>
|
||||
<size>9</size>
|
||||
<!-- font size in points -->
|
||||
<weight>normal</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="MenuItem">
|
||||
<name>sans</name>
|
||||
<size>9</size>
|
||||
<!-- font size in points -->
|
||||
<weight>normal</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="ActiveOnScreenDisplay">
|
||||
<name>sans</name>
|
||||
<size>9</size>
|
||||
<!-- font size in points -->
|
||||
<weight>bold</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
<font place="InactiveOnScreenDisplay">
|
||||
<name>sans</name>
|
||||
<size>9</size>
|
||||
<!-- font size in points -->
|
||||
<weight>bold</weight>
|
||||
<!-- 'bold' or 'normal' -->
|
||||
<slant>normal</slant>
|
||||
<!-- 'italic' or 'normal' -->
|
||||
</font>
|
||||
</theme>
|
||||
|
||||
<desktops>
|
||||
<!-- this stuff is only used at startup, pagers allow you to change them
|
||||
during a session
|
||||
|
||||
these are default values to use when other ones are not already set
|
||||
by other applications, or saved in your session
|
||||
|
||||
use obconf if you want to change these without having to log out
|
||||
and back in -->
|
||||
<number>1</number>
|
||||
<firstdesk>1</firstdesk>
|
||||
<names>
|
||||
<!-- set names up here if you want to, like this:
|
||||
<name>desktop 1</name>
|
||||
<name>desktop 2</name>
|
||||
-->
|
||||
</names>
|
||||
<popupTime>875</popupTime>
|
||||
<!-- The number of milliseconds to show the popup for when switching
|
||||
desktops. Set this to 0 to disable the popup. -->
|
||||
</desktops>
|
||||
|
||||
<resize>
|
||||
<drawContents>yes</drawContents>
|
||||
<popupShow>Nonpixel</popupShow>
|
||||
<!-- 'Always', 'Never', or 'Nonpixel' (xterms and such) -->
|
||||
<popupPosition>Center</popupPosition>
|
||||
<!-- 'Center', 'Top', or 'Fixed' -->
|
||||
<popupFixedPosition>
|
||||
<!-- these are used if popupPosition is set to 'Fixed' -->
|
||||
|
||||
<x>10</x>
|
||||
<!-- positive number for distance from left edge, negative number for
|
||||
distance from right edge, or 'Center' -->
|
||||
<y>10</y>
|
||||
<!-- positive number for distance from top edge, negative number for
|
||||
distance from bottom edge, or 'Center' -->
|
||||
</popupFixedPosition>
|
||||
</resize>
|
||||
|
||||
<!-- You can reserve a portion of your screen where windows will not cover when
|
||||
they are maximized, or when they are initially placed.
|
||||
Many programs reserve space automatically, but you can use this in other
|
||||
cases. -->
|
||||
<margins>
|
||||
<top>0</top>
|
||||
<bottom>0</bottom>
|
||||
<left>0</left>
|
||||
<right>0</right>
|
||||
</margins>
|
||||
|
||||
<dock>
|
||||
<position>TopLeft</position>
|
||||
<!-- (Top|Bottom)(Left|Right|)|Top|Bottom|Left|Right|Floating -->
|
||||
<floatingX>0</floatingX>
|
||||
<floatingY>0</floatingY>
|
||||
<noStrut>no</noStrut>
|
||||
<stacking>Above</stacking>
|
||||
<!-- 'Above', 'Normal', or 'Below' -->
|
||||
<direction>Vertical</direction>
|
||||
<!-- 'Vertical' or 'Horizontal' -->
|
||||
<autoHide>no</autoHide>
|
||||
<hideDelay>300</hideDelay>
|
||||
<!-- in milliseconds (1000 = 1 second) -->
|
||||
<showDelay>300</showDelay>
|
||||
<!-- in milliseconds (1000 = 1 second) -->
|
||||
<moveButton>Middle</moveButton>
|
||||
<!-- 'Left', 'Middle', 'Right' -->
|
||||
</dock>
|
||||
|
||||
<keyboard>
|
||||
<chainQuitKey>C-g</chainQuitKey>
|
||||
|
||||
<!-- Keybindings for desktop switching -->
|
||||
<keybind key="C-A-Left">
|
||||
<action name="GoToDesktop"><to>left</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="C-A-Right">
|
||||
<action name="GoToDesktop"><to>right</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="C-A-Up">
|
||||
<action name="GoToDesktop"><to>up</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="C-A-Down">
|
||||
<action name="GoToDesktop"><to>down</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="S-A-Left">
|
||||
<action name="SendToDesktop"><to>left</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="S-A-Right">
|
||||
<action name="SendToDesktop"><to>right</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="S-A-Up">
|
||||
<action name="SendToDesktop"><to>up</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="S-A-Down">
|
||||
<action name="SendToDesktop"><to>down</to><wrap>no</wrap></action>
|
||||
</keybind>
|
||||
<keybind key="W-F1">
|
||||
<action name="GoToDesktop"><to>1</to></action>
|
||||
</keybind>
|
||||
<keybind key="W-F2">
|
||||
<action name="GoToDesktop"><to>2</to></action>
|
||||
</keybind>
|
||||
<keybind key="W-F3">
|
||||
<action name="GoToDesktop"><to>3</to></action>
|
||||
</keybind>
|
||||
<keybind key="W-F4">
|
||||
<action name="GoToDesktop"><to>4</to></action>
|
||||
</keybind>
|
||||
<keybind key="W-d">
|
||||
<action name="ToggleShowDesktop"/>
|
||||
</keybind>
|
||||
|
||||
<!-- Keybindings for windows -->
|
||||
<keybind key="A-F4">
|
||||
<action name="Close"/>
|
||||
</keybind>
|
||||
<keybind key="A-Escape">
|
||||
<action name="Lower"/>
|
||||
<action name="FocusToBottom"/>
|
||||
<action name="Unfocus"/>
|
||||
</keybind>
|
||||
<keybind key="A-space">
|
||||
<!--action name="ShowMenu"><menu>client-menu</menu></action-->
|
||||
</keybind>
|
||||
<!-- Take a screenshot of the current window with scrot when Alt+Print are pressed -->
|
||||
<keybind key="A-Print">
|
||||
<action name="Execute"><command>scrot -s</command></action>
|
||||
</keybind>
|
||||
|
||||
<!-- Keybindings for window switching -->
|
||||
<keybind key="A-Tab">
|
||||
<action name="NextWindow">
|
||||
<finalactions>
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</finalactions>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="A-S-Tab">
|
||||
<action name="PreviousWindow">
|
||||
<finalactions>
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</finalactions>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="C-A-Tab">
|
||||
<action name="NextWindow">
|
||||
<panels>yes</panels><desktop>yes</desktop>
|
||||
<finalactions>
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</finalactions>
|
||||
</action>
|
||||
</keybind>
|
||||
|
||||
<!-- Keybindings for window switching with the arrow keys -->
|
||||
<keybind key="W-S-Right">
|
||||
<action name="DirectionalCycleWindows">
|
||||
<direction>right</direction>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="W-S-Left">
|
||||
<action name="DirectionalCycleWindows">
|
||||
<direction>left</direction>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="W-S-Up">
|
||||
<action name="DirectionalCycleWindows">
|
||||
<direction>up</direction>
|
||||
</action>
|
||||
</keybind>
|
||||
<keybind key="W-S-Down">
|
||||
<action name="DirectionalCycleWindows">
|
||||
<direction>down</direction>
|
||||
</action>
|
||||
</keybind>
|
||||
|
||||
<!-- Keybindings for running applications -->
|
||||
<keybind key="W-e">
|
||||
<action name="Execute">
|
||||
<startupnotify>
|
||||
<enabled>true</enabled>
|
||||
<name>Konqueror</name>
|
||||
</startupnotify>
|
||||
<command>kfmclient openProfile filemanagement</command>
|
||||
</action>
|
||||
</keybind>
|
||||
<!-- Launch scrot when Print is pressed -->
|
||||
<keybind key="Print">
|
||||
<action name="Execute"><command>scrot</command></action>
|
||||
</keybind>
|
||||
</keyboard>
|
||||
|
||||
<mouse>
|
||||
<dragThreshold>1</dragThreshold>
|
||||
<!-- number of pixels the mouse must move before a drag begins -->
|
||||
<doubleClickTime>500</doubleClickTime>
|
||||
<!-- in milliseconds (1000 = 1 second) -->
|
||||
<screenEdgeWarpTime>400</screenEdgeWarpTime>
|
||||
<!-- Time before changing desktops when the pointer touches the edge of the
|
||||
screen while moving a window, in milliseconds (1000 = 1 second).
|
||||
Set this to 0 to disable warping -->
|
||||
<screenEdgeWarpMouse>false</screenEdgeWarpMouse>
|
||||
<!-- Set this to TRUE to move the mouse pointer across the desktop when
|
||||
switching due to hitting the edge of the screen -->
|
||||
|
||||
<context name="Frame">
|
||||
<mousebind button="A-Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="A-Left" action="Click">
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="A-Left" action="Drag">
|
||||
<action name="Move"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="A-Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="A-Right" action="Drag">
|
||||
<action name="Resize"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="A-Middle" action="Press">
|
||||
<action name="Lower"/>
|
||||
<action name="FocusToBottom"/>
|
||||
<action name="Unfocus"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="C-A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="C-A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-S-Up" action="Click">
|
||||
<action name="SendToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-S-Down" action="Click">
|
||||
<action name="SendToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Titlebar">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Move"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="DoubleClick">
|
||||
<action name="ToggleMaximize"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="Up" action="Click">
|
||||
<action name="if">
|
||||
<shaded>no</shaded>
|
||||
<then>
|
||||
<action name="Shade"/>
|
||||
<action name="FocusToBottom"/>
|
||||
<action name="Unfocus"/>
|
||||
<action name="Lower"/>
|
||||
</then>
|
||||
</action>
|
||||
</mousebind>
|
||||
<mousebind button="Down" action="Click">
|
||||
<action name="if">
|
||||
<shaded>yes</shaded>
|
||||
<then>
|
||||
<action name="Unshade"/>
|
||||
<action name="Raise"/>
|
||||
</then>
|
||||
</action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Titlebar Top Right Bottom Left TLCorner TRCorner BRCorner BLCorner">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="Middle" action="Press">
|
||||
<action name="Lower"/>
|
||||
<action name="FocusToBottom"/>
|
||||
<action name="Unfocus"/>
|
||||
</mousebind>
|
||||
|
||||
<!--mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||
</mousebind-->
|
||||
</context>
|
||||
|
||||
<context name="Top">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"><edge>top</edge></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Left">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"><edge>left</edge></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Right">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"><edge>right</edge></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Bottom">
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"><edge>bottom</edge></action>
|
||||
</mousebind>
|
||||
|
||||
<!--mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||
</mousebind-->
|
||||
</context>
|
||||
|
||||
<context name="TRCorner BRCorner TLCorner BLCorner">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Drag">
|
||||
<action name="Resize"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Client">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Middle" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Icon">
|
||||
<!--mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||
</mousebind-->
|
||||
</context>
|
||||
|
||||
<context name="AllDesktops">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="ToggleOmnipresent"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Shade">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="ToggleShade"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Iconify">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="Iconify"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Maximize">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Middle" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="ToggleMaximize"/>
|
||||
</mousebind>
|
||||
<mousebind button="Middle" action="Click">
|
||||
<action name="ToggleMaximize"><direction>vertical</direction></action>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Click">
|
||||
<action name="ToggleMaximize"><direction>horizontal</direction></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Close">
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
<action name="Unshade"/>
|
||||
</mousebind>
|
||||
<mousebind button="Left" action="Click">
|
||||
<action name="Close"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Desktop">
|
||||
<mousebind button="Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="C-A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="C-A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
|
||||
<mousebind button="Left" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="Focus"/>
|
||||
<action name="Raise"/>
|
||||
</mousebind>
|
||||
</context>
|
||||
|
||||
<context name="Root">
|
||||
<!-- Menus -->
|
||||
<!--mousebind button="Middle" action="Press">
|
||||
<action name="ShowMenu"><menu>client-list-combined-menu</menu></action>
|
||||
</mousebind>
|
||||
<mousebind button="Right" action="Press">
|
||||
<action name="ShowMenu"><menu>root-menu</menu></action>
|
||||
</mousebind-->
|
||||
</context>
|
||||
|
||||
<context name="MoveResize">
|
||||
<mousebind button="Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-Up" action="Click">
|
||||
<action name="GoToDesktop"><to>previous</to></action>
|
||||
</mousebind>
|
||||
<mousebind button="A-Down" action="Click">
|
||||
<action name="GoToDesktop"><to>next</to></action>
|
||||
</mousebind>
|
||||
</context>
|
||||
</mouse>
|
||||
|
||||
<menu>
|
||||
<!-- You can specify more than one menu file in here and they are all loaded,
|
||||
just don't make menu ids clash or, well, it'll be kind of pointless -->
|
||||
|
||||
<!-- default menu file (or custom one in $HOME/.config/openbox/) -->
|
||||
<!-- system menu files on Debian systems -->
|
||||
<!--file>/var/lib/openbox/debian-menu.xml</file-->
|
||||
<file>menu.xml</file>
|
||||
<hideDelay>200</hideDelay>
|
||||
<!-- if a press-release lasts longer than this setting (in milliseconds), the
|
||||
menu is hidden again -->
|
||||
<middle>no</middle>
|
||||
<!-- center submenus vertically about the parent entry -->
|
||||
<submenuShowDelay>100</submenuShowDelay>
|
||||
<!-- time to delay before showing a submenu after hovering over the parent
|
||||
entry.
|
||||
if this is a negative value, then the delay is infinite and the
|
||||
submenu will not be shown until it is clicked on -->
|
||||
<submenuHideDelay>400</submenuHideDelay>
|
||||
<!-- time to delay before hiding a submenu when selecting another
|
||||
entry in parent menu
|
||||
if this is a negative value, then the delay is infinite and the
|
||||
submenu will not be hidden until a different submenu is opened -->
|
||||
<showIcons>yes</showIcons>
|
||||
<!-- controls if icons appear in the client-list-(combined-)menu -->
|
||||
<manageDesktops>yes</manageDesktops>
|
||||
<!-- show the manage desktops section in the client-list-(combined-)menu -->
|
||||
</menu>
|
||||
|
||||
<applications>
|
||||
<!--
|
||||
# this is an example with comments through out. use these to make your
|
||||
# own rules, but without the comments of course.
|
||||
# you may use one or more of the name/class/role/title/type rules to specify
|
||||
# windows to match
|
||||
|
||||
<application name="the window's _OB_APP_NAME property (see obxprop)"
|
||||
class="the window's _OB_APP_CLASS property (see obxprop)"
|
||||
groupname="the window's _OB_APP_GROUP_NAME property (see obxprop)"
|
||||
groupclass="the window's _OB_APP_GROUP_CLASS property (see obxprop)"
|
||||
role="the window's _OB_APP_ROLE property (see obxprop)"
|
||||
title="the window's _OB_APP_TITLE property (see obxprop)"
|
||||
type="the window's _OB_APP_TYPE property (see obxprob)..
|
||||
(if unspecified, then it is 'dialog' for child windows)">
|
||||
# you may set only one of name/class/role/title/type, or you may use more
|
||||
# than one together to restrict your matches.
|
||||
|
||||
# the name, class, role, and title use simple wildcard matching such as those
|
||||
# used by a shell. you can use * to match any characters and ? to match
|
||||
# any single character.
|
||||
|
||||
# the type is one of: normal, dialog, splash, utility, menu, toolbar, dock,
|
||||
# or desktop
|
||||
|
||||
# when multiple rules match a window, they will all be applied, in the
|
||||
# order that they appear in this list
|
||||
|
||||
|
||||
# each rule element can be left out or set to 'default' to specify to not
|
||||
# change that attribute of the window
|
||||
|
||||
<decor>yes</decor>
|
||||
# enable or disable window decorations
|
||||
|
||||
<shade>no</shade>
|
||||
# make the window shaded when it appears, or not
|
||||
|
||||
<position force="no">
|
||||
# the position is only used if both an x and y coordinate are provided
|
||||
# (and not set to 'default')
|
||||
# when force is "yes", then the window will be placed here even if it
|
||||
# says you want it placed elsewhere. this is to override buggy
|
||||
# applications who refuse to behave
|
||||
<x>center</x>
|
||||
# a number like 50, or 'center' to center on screen. use a negative number
|
||||
# to start from the right (or bottom for <y>), ie -50 is 50 pixels from
|
||||
# the right edge (or bottom). use 'default' to specify using value
|
||||
# provided by the application, or chosen by openbox, instead.
|
||||
<y>200</y>
|
||||
<monitor>1</monitor>
|
||||
# specifies the monitor in a xinerama setup.
|
||||
# 1 is the first head, or 'mouse' for wherever the mouse is
|
||||
</position>
|
||||
|
||||
<size>
|
||||
# the size to make the window.
|
||||
<width>20</width>
|
||||
# a number like 20, or 'default' to use the size given by the application.
|
||||
# you can use fractions such as 1/2 or percentages such as 75% in which
|
||||
# case the value is relative to the size of the monitor that the window
|
||||
# appears on.
|
||||
<height>30%</height>
|
||||
</size>
|
||||
|
||||
<focus>yes</focus>
|
||||
# if the window should try be given focus when it appears. if this is set
|
||||
# to yes it doesn't guarantee the window will be given focus. some
|
||||
# restrictions may apply, but Openbox will try to
|
||||
|
||||
<desktop>1</desktop>
|
||||
# 1 is the first desktop, 'all' for all desktops
|
||||
|
||||
<layer>normal</layer>
|
||||
# 'above', 'normal', or 'below'
|
||||
|
||||
<iconic>no</iconic>
|
||||
# make the window iconified when it appears, or not
|
||||
|
||||
<skip_pager>no</skip_pager>
|
||||
# asks to not be shown in pagers
|
||||
|
||||
<skip_taskbar>no</skip_taskbar>
|
||||
# asks to not be shown in taskbars. window cycling actions will also
|
||||
# skip past such windows
|
||||
|
||||
<fullscreen>yes</fullscreen>
|
||||
# make the window in fullscreen mode when it appears
|
||||
|
||||
<maximized>true</maximized>
|
||||
# 'Horizontal', 'Vertical' or boolean (yes/no)
|
||||
</application>
|
||||
|
||||
# end of the example
|
||||
-->
|
||||
</applications>
|
||||
|
||||
</openbox_config>
|
39
.docker/vivaldi/policies.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"AutofillAddressEnabled": false,
|
||||
"AutofillCreditCardEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
"DefaultNotificationsSetting": 2,
|
||||
"DeveloperToolsAvailability": 2,
|
||||
"EditBookmarksEnabled": false,
|
||||
"FullscreenAllowed": true,
|
||||
"IncognitoModeAvailability": 1,
|
||||
"SyncDisabled": true,
|
||||
"AutoplayAllowed": true,
|
||||
"BrowserAddPersonEnabled": false,
|
||||
"BrowserGuestModeEnabled": false,
|
||||
"DefaultPopupsSetting": 2,
|
||||
"DownloadRestrictions": 3,
|
||||
"VideoCaptureAllowed": true,
|
||||
"AllowFileSelectionDialogs": false,
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
],
|
||||
"ExtensionInstallForcelist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
|
||||
],
|
||||
"ExtensionInstallAllowlist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
|
||||
"mnjggcdmjocbbbhaepdhchncahnbgone"
|
||||
],
|
||||
"ExtensionInstallBlocklist": [
|
||||
"*"
|
||||
]
|
||||
}
|
339
.docker/vivaldi/preferences.json
Normal file
@ -0,0 +1,339 @@
|
||||
{
|
||||
"homepage": "http://www.google.com",
|
||||
"homepage_is_newtabpage": false,
|
||||
"first_run_tabs": [
|
||||
"https://www.google.com/_/chrome/newtab?ie=UTF-8"
|
||||
],
|
||||
"custom_links": {
|
||||
"initialized": true,
|
||||
"list": [
|
||||
{
|
||||
"title": "YouTube",
|
||||
"url": "https://www.youtube.com/"
|
||||
},
|
||||
{
|
||||
"title": "Netflix",
|
||||
"url": "https://netflix.com"
|
||||
},
|
||||
{
|
||||
"title": "Hulu",
|
||||
"url": "https://www.hulu.com/"
|
||||
},
|
||||
{
|
||||
"title": "9Anime",
|
||||
"url": "https://9anime.to/"
|
||||
},
|
||||
{
|
||||
"title": "Crunchy Roll",
|
||||
"url": "https://www.crunchyroll.com/"
|
||||
},
|
||||
{
|
||||
"title": "Funimation",
|
||||
"url": "https://www.funimation.com/"
|
||||
},
|
||||
{
|
||||
"title": "Disney+",
|
||||
"url": "https://www.disneyplus.com/"
|
||||
},
|
||||
{
|
||||
"title": "HBO Now",
|
||||
"url": "https://play.hbonow.com/"
|
||||
},
|
||||
{
|
||||
"title": "Amazon Video",
|
||||
"url": "https://www.amazon.com/Amazon-Video/b?node=2858778011"
|
||||
},
|
||||
{
|
||||
"title": "VRV",
|
||||
"url": "https://vrv.co/"
|
||||
},
|
||||
{
|
||||
"title": "Twitch",
|
||||
"url": "https://www.twitch.tv/"
|
||||
},
|
||||
{
|
||||
"title": "Mixer",
|
||||
"url": "https://mixer.com/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"browser": {
|
||||
"custom_chrome_frame": false,
|
||||
"show_home_button": true,
|
||||
"window_placement": {
|
||||
"maximized": true
|
||||
}
|
||||
},
|
||||
"bookmark_bar": {
|
||||
"show_on_all_tabs": false
|
||||
},
|
||||
"sync_promo": {
|
||||
"show_on_first_run_allowed": false
|
||||
},
|
||||
"default_search_provider_data": {
|
||||
"image_template_url_data": {
|
||||
"alternate_urls": ["https://www.google.com/#q={searchTerms}", "https://www.google.com/search#q={searchTerms}", "https://www.google.com/webhp#q={searchTerms}", "https://www.google.com/s#q={searchTerms}", "https://www.google.com/s?q={searchTerms}"],
|
||||
"contextual_search_url": "",
|
||||
"created_by_policy": false,
|
||||
"created_from_play_api": false,
|
||||
"date_created": "0",
|
||||
"doodle_url": "",
|
||||
"favicon_url": "https://www.google.com/favicon.ico",
|
||||
"id": "11",
|
||||
"image_url": "https://www.google.com/searchbyimage/upload",
|
||||
"image_url_post_params": "encoded_image={google:imageThumbnail},image_url={google:imageURL},sbisrc={google:imageSearchSource},original_width={google:imageOriginalWidth},original_height={google:imageOriginalHeight}",
|
||||
"input_encodings": ["UTF-8"],
|
||||
"is_active": 0,
|
||||
"keyword": "g",
|
||||
"last_modified": "0",
|
||||
"last_visited": "0",
|
||||
"logo_url": "",
|
||||
"new_tab_url": "",
|
||||
"originating_url": "",
|
||||
"position": "IhxEc1dKb3ZJanppM1NCWTZhRHNGN0ljYkRHeVk9",
|
||||
"preconnect_to_search_url": false,
|
||||
"prepopulate_id": 1,
|
||||
"safe_for_autoreplace": true,
|
||||
"search_url_post_params": "",
|
||||
"short_name": "Google",
|
||||
"side_search_param": "",
|
||||
"suggestions_url": "",
|
||||
"suggestions_url_post_params": "",
|
||||
"url": "https://www.google.com/search?q={searchTerms}&{google:originalQueryForSuggestion}{google:prefetchSource}{google:sourceId}{google:contextualSearchVersion}ie={inputEncoding}",
|
||||
"usage_count": 0
|
||||
},
|
||||
"private_template_url_data": {
|
||||
"alternate_urls": [],
|
||||
"contextual_search_url": "",
|
||||
"created_by_policy": false,
|
||||
"created_from_play_api": false,
|
||||
"date_created": "0",
|
||||
"doodle_url": "",
|
||||
"favicon_url": "https://duckduckgo.com/favicon.ico",
|
||||
"id": "4",
|
||||
"image_url": "",
|
||||
"image_url_post_params": "",
|
||||
"input_encodings": ["UTF-8"],
|
||||
"is_active": 0,
|
||||
"keyword": "d",
|
||||
"last_modified": "0",
|
||||
"last_visited": "0",
|
||||
"logo_url": "",
|
||||
"new_tab_url": "",
|
||||
"originating_url": "",
|
||||
"position": "Ih3bMkFheVB5WitEdGZmeU9hV0d5K3RGUXRhR3RFPQ==",
|
||||
"preconnect_to_search_url": false,
|
||||
"prepopulate_id": 7,
|
||||
"safe_for_autoreplace": true,
|
||||
"search_url_post_params": "",
|
||||
"short_name": "DuckDuckGo",
|
||||
"side_search_param": "",
|
||||
"suggestions_url": "https://duckduckgo.com/ac/?q={searchTerms}&type=list",
|
||||
"suggestions_url_post_params": "",
|
||||
"url": "https://duckduckgo.com/?q={searchTerms}&{ddg:Referral}",
|
||||
"usage_count": 0
|
||||
},
|
||||
"speeddials_template_url_data": {
|
||||
"alternate_urls": ["https://www.google.com/#q={searchTerms}", "https://www.google.com/search#q={searchTerms}", "https://www.google.com/webhp#q={searchTerms}", "https://www.google.com/s#q={searchTerms}", "https://www.google.com/s?q={searchTerms}"],
|
||||
"contextual_search_url": "",
|
||||
"created_by_policy": false,
|
||||
"created_from_play_api": false,
|
||||
"date_created": "0",
|
||||
"doodle_url": "",
|
||||
"favicon_url": "https://www.google.com/favicon.ico",
|
||||
"id": "11",
|
||||
"image_url": "https://www.google.com/searchbyimage/upload",
|
||||
"image_url_post_params": "encoded_image={google:imageThumbnail},image_url={google:imageURL},sbisrc={google:imageSearchSource},original_width={google:imageOriginalWidth},original_height={google:imageOriginalHeight}",
|
||||
"input_encodings": ["UTF-8"],
|
||||
"is_active": 0,
|
||||
"keyword": "g",
|
||||
"last_modified": "0",
|
||||
"last_visited": "0",
|
||||
"logo_url": "",
|
||||
"new_tab_url": "",
|
||||
"originating_url": "",
|
||||
"position": "IhxEc1dKb3ZJanppM1NCWTZhRHNGN0ljYkRHeVk9",
|
||||
"preconnect_to_search_url": false,
|
||||
"prepopulate_id": 1,
|
||||
"safe_for_autoreplace": true,
|
||||
"search_url_post_params": "",
|
||||
"short_name": "Google",
|
||||
"side_search_param": "",
|
||||
"suggestions_url": "",
|
||||
"suggestions_url_post_params": "",
|
||||
"url": "https://www.google.com/search?q={searchTerms}&{google:originalQueryForSuggestion}{google:prefetchSource}{google:sourceId}{google:contextualSearchVersion}ie={inputEncoding}",
|
||||
"usage_count": 0
|
||||
},
|
||||
"template_url_data": {
|
||||
"alternate_urls": ["https://www.google.com/#q={searchTerms}", "https://www.google.com/search#q={searchTerms}", "https://www.google.com/webhp#q={searchTerms}", "https://www.google.com/s#q={searchTerms}", "https://www.google.com/s?q={searchTerms}"],
|
||||
"contextual_search_url": "",
|
||||
"created_by_policy": false,
|
||||
"created_from_play_api": false,
|
||||
"date_created": "0",
|
||||
"doodle_url": "",
|
||||
"favicon_url": "https://www.google.com/favicon.ico",
|
||||
"id": "11",
|
||||
"image_url": "https://www.google.com/searchbyimage/upload",
|
||||
"image_url_post_params": "encoded_image={google:imageThumbnail},image_url={google:imageURL},sbisrc={google:imageSearchSource},original_width={google:imageOriginalWidth},original_height={google:imageOriginalHeight}",
|
||||
"input_encodings": ["UTF-8"],
|
||||
"is_active": 0,
|
||||
"keyword": "g",
|
||||
"last_modified": "0",
|
||||
"last_visited": "0",
|
||||
"logo_url": "",
|
||||
"new_tab_url": "",
|
||||
"originating_url": "",
|
||||
"position": "IhxEc1dKb3ZJanppM1NCWTZhRHNGN0ljYkRHeVk9",
|
||||
"preconnect_to_search_url": false,
|
||||
"prepopulate_id": 1,
|
||||
"safe_for_autoreplace": true,
|
||||
"search_url_post_params": "",
|
||||
"short_name": "Google",
|
||||
"side_search_param": "",
|
||||
"suggestions_url": "",
|
||||
"suggestions_url_post_params": "",
|
||||
"url": "https://www.google.com/search?q={searchTerms}&{google:originalQueryForSuggestion}{google:prefetchSource}{google:sourceId}{google:contextualSearchVersion}ie={inputEncoding}",
|
||||
"usage_count": 0
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"import_bookmarks_from_file": "bookmarks.html",
|
||||
"import_bookmarks": true,
|
||||
"import_history": true,
|
||||
"import_home_page": true,
|
||||
"import_search_engine": true,
|
||||
"ping_delay": 60,
|
||||
"do_not_create_desktop_shortcut": true,
|
||||
"do_not_create_quick_launch_shortcut": true,
|
||||
"do_not_create_taskbar_shortcut": true,
|
||||
"do_not_launch_chrome": true,
|
||||
"do_not_register_for_update_launch": true,
|
||||
"make_chrome_default": true,
|
||||
"make_chrome_default_for_user": true,
|
||||
"system_level": false,
|
||||
"verbose_logging": false
|
||||
},
|
||||
"enable_do_not_track": true,
|
||||
"profile": {
|
||||
"avatar_index": 34,
|
||||
"default_content_setting_values": {
|
||||
"clipboard": 2,
|
||||
"cookies": 4,
|
||||
"geolocation": 2,
|
||||
"media_stream_camera": 2,
|
||||
"media_stream_mic": 2,
|
||||
"midi_sysex": 2,
|
||||
"payment_handler": 2,
|
||||
"usb_guard": 2
|
||||
},
|
||||
"name": "Neko",
|
||||
"using_default_avatar": false,
|
||||
"using_default_name": false,
|
||||
"using_gaia_avatar": false
|
||||
},
|
||||
"signin": {
|
||||
"allowed": false
|
||||
},
|
||||
"vivaldi": {
|
||||
"address_bar": {
|
||||
"autocomplete": {
|
||||
"enabled": false
|
||||
},
|
||||
"omnibox": {
|
||||
"show_browser_history": false,
|
||||
"show_search_history": false,
|
||||
"show_typed_history": false
|
||||
},
|
||||
"search": {
|
||||
"display": 1,
|
||||
"in_new_tab": false
|
||||
},
|
||||
"show_full_url": true,
|
||||
"show_qr_generator": true,
|
||||
"visible": true
|
||||
},
|
||||
"bookmarks": {
|
||||
"deleted_partners": [
|
||||
"f79cd6e8-ebc0-444d-ac96-00da456dcb59",
|
||||
"d680347f-1073-46b9-a546-ae0238e7b9d9"
|
||||
],
|
||||
"language": "en-US",
|
||||
"version": "24"
|
||||
},
|
||||
"downloads": {
|
||||
"notify_on_complete": false,
|
||||
"open_panel_on_new": false,
|
||||
"start_automatically": false,
|
||||
"update_default_download_when_saving_as": false
|
||||
},
|
||||
"history": {
|
||||
"days_to_keep_visits": 0
|
||||
},
|
||||
"homepage": "vivaldi://startpage",
|
||||
"incognito": {
|
||||
"show_intro": false
|
||||
},
|
||||
"language_at_install": "en-US",
|
||||
"menu": {
|
||||
"icon_type": 1
|
||||
},
|
||||
"mouse_gestures": {
|
||||
"enabled": false,
|
||||
"rocker_gestures": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"popups": {
|
||||
"show_in_tab": true
|
||||
},
|
||||
"settings": {
|
||||
"in_tab": true
|
||||
},
|
||||
"startpage": {
|
||||
"navigation": 1,
|
||||
"speed_dial": {
|
||||
"add_button_visible": false,
|
||||
"allow_dnd": false,
|
||||
"columns": 4,
|
||||
"delete_visible": false,
|
||||
"display_search": true,
|
||||
"privacy_stats_show": false,
|
||||
"tracker_suggestion_show": false,
|
||||
"width": 170
|
||||
}
|
||||
},
|
||||
"startup": {
|
||||
"check_is_default": false,
|
||||
"has_seen_feature": 1
|
||||
},
|
||||
"status_bar": {
|
||||
"display": 0,
|
||||
"minimized": 0
|
||||
},
|
||||
"system": {
|
||||
"show_exit_confirmation_dialog": true
|
||||
},
|
||||
"tabs": {
|
||||
"new_placement": 3,
|
||||
"open_new_in_background": false,
|
||||
"stacking": {
|
||||
"open_accordions": []
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"schedule": {
|
||||
"o_s": {
|
||||
"dark": "Vivaldi2",
|
||||
"light": "Vivaldi2"
|
||||
}
|
||||
},
|
||||
"use_animation": false
|
||||
},
|
||||
"translate": {
|
||||
"enabled": false
|
||||
},
|
||||
"windows": {
|
||||
"use_native_decoration": true
|
||||
}
|
||||
}
|
||||
}
|
22
.docker/vivaldi/supervisord.conf
Normal file
@ -0,0 +1,22 @@
|
||||
[program:vivaldi-stable]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/vivaldi-stable --no-sandbox --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/vivaldi --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=800
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/vivaldi.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
||||
|
||||
[program:openbox]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
|
||||
autorestart=true
|
||||
priority=300
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/openbox.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
@ -1,18 +0,0 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
#
|
||||
# install vncviewer
|
||||
RUN set -eux; apt-get update; \
|
||||
apt-get install -y --no-install-recommends openbox xtightvncviewer; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
ENV NEKO_VNC_URL=""
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.conf /etc/neko/supervisord/vncviewer.conf
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: [ m1k1o ]
|
4
.github/workflows/build.yml
vendored
@ -47,8 +47,10 @@ jobs:
|
||||
if: github.repository_owner == 'm1k1o'
|
||||
needs: [ build-base ]
|
||||
strategy:
|
||||
# Will build all images even if some fail.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tags: [ firefox, chromium, google-chrome, ungoogled-chromium, brave, tor-browser, vncviewer, vlc, xfce ]
|
||||
tags: [ firefox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, vivaldi, opera, tor-browser, remmina, vlc, xfce ]
|
||||
env:
|
||||
DOCKER_TAG: ${{ matrix.tags }}
|
||||
steps:
|
||||
|
133
.github/workflows/tags.yml
vendored
Normal file
@ -0,0 +1,133 @@
|
||||
name: "CI for version tags"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: m1k1o/neko
|
||||
|
||||
jobs:
|
||||
build-base:
|
||||
runs-on: ubuntu-latest
|
||||
#
|
||||
# do not run on forks
|
||||
#
|
||||
if: github.repository_owner == 'm1k1o'
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Extract metadata (tags, labels) for Docker
|
||||
uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/base
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha,format=long
|
||||
-
|
||||
name: Log in to the Container registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_ACCESS_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: .docker/base/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
#
|
||||
# do not run on forks
|
||||
#
|
||||
if: github.repository_owner == 'm1k1o'
|
||||
needs: [ build-base ]
|
||||
strategy:
|
||||
# Will build all images even if some fail.
|
||||
matrix:
|
||||
include:
|
||||
- tag: firefox
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: chromium
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: google-chrome
|
||||
platforms: linux/amd64
|
||||
- tag: ungoogled-chromium
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: microsoft-edge
|
||||
platforms: linux/amd64
|
||||
- tag: brave
|
||||
platforms: linux/amd64
|
||||
- tag: vivaldi
|
||||
platforms: linux/amd64
|
||||
- tag: opera
|
||||
platforms: linux/amd64
|
||||
- tag: tor-browser
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: remmina
|
||||
platforms: linux/amd64
|
||||
- tag: vlc
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: xfce
|
||||
platforms: linux/amd64,linux/arm64
|
||||
env:
|
||||
TAG_NAME: ${{ matrix.tag }}
|
||||
PLATFORMS: ${{ matrix.platforms }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Extract metadata (tags, labels) for Docker
|
||||
uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
-
|
||||
name: Log in to the Container registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_ACCESS_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .docker/${{ env.TAG_NAME }}
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/base:sha-${{ github.sha }}
|
1
.vscode/settings.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{}
|
104
README.md
@ -1,12 +1,23 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/m1k1o/neko" title="Neko's Github repository.">
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="450" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="400" height="auto"/>
|
||||
</a>
|
||||
<p align="center">
|
||||
<a href="https://github.com/m1k1o/neko/releases">
|
||||
<img src="https://img.shields.io/github/v/release/m1k1o/neko" alt="release">
|
||||
</a>
|
||||
<a href="https://github.com/m1k1o/neko/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/m1k1o/neko" alt="license">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/u/m1k1o/neko">
|
||||
<img src="https://img.shields.io/docker/pulls/m1k1o/neko" alt="pulls">
|
||||
</a>
|
||||
<a href="https://github.com/m1k1o/neko/issues">
|
||||
<img src="https://img.shields.io/github/issues/m1k1o/neko" alt="issues">
|
||||
</a>
|
||||
<a href="https://github.com/sponsors/m1k1o">
|
||||
<img src="https://img.shields.io/badge/-sponsor-red" alt="issues">
|
||||
</a>
|
||||
<a href="https://discord.gg/3U6hWpC">
|
||||
<img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord">
|
||||
</a>
|
||||
@ -14,11 +25,7 @@
|
||||
<img src="https://github.com/m1k1o/neko/actions/workflows/build.yml/badge.svg" alt="build">
|
||||
</a>
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<img src="https://i.imgur.com/ZSzbQr7.gif" width="650" height="auto"/>
|
||||
<br/>
|
||||
<br/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/intro.gif" width="650" height="auto"/>
|
||||
</div>
|
||||
|
||||
# n.eko
|
||||
@ -27,14 +34,85 @@ This app uses Web RTC to stream a desktop inside of a docker container, original
|
||||
|
||||
Then I found [this](https://github.com/nurdism/neko) project and started to dig into it. I really liked the idea of having collaborative browser browsing together with mutliple people, so I created a fork. Initially, I wanted to merge my changes to the upstream repository, but the original author did not have time for this project anymore and it got eventually archived.
|
||||
|
||||
## Use-cases and comparison
|
||||
|
||||
Neko started as a virtual browser that is streamed using WebRTC to multiple users.
|
||||
- It is **not only limited to a browser**; it can run anything that runs on linux (e.g. VLC). Browser only happens to be the most popular and widely used use-case.
|
||||
- In fact, it is not limited to a single program either; you can install a full desktop environment (e.g. XFCE).
|
||||
- Speaking of limits, it does not need to run in a container; you could install neko on your host, connect to your X server and control your whole VM.
|
||||
- Theoretically it is not limited to only X server, anything that can be controlled and scraped periodically for images could be used instead.
|
||||
- Like implementing RDP or VNC protocol, where neko would only act as WebRTC relay server. This is currently only future.
|
||||
|
||||
Primary use case is connecting with multiple people, leveraging real time synchronization and interactivity:
|
||||
- **Watch party** - watching video content together with multiple people and reacting to it (chat, emotes) - open source alternative to [giggl.app](https://giggl.app/) or [hyperbeam](https://watch.hyperbeam.com).
|
||||
- **Interactive presentation** - not only screen sharing, but others can control the screen.
|
||||
- **Collaborative tool** - brainstorming ideas, cobrowsing, code debugging together.
|
||||
- **Support/Teaching** - interactively guiding people in controlled environment.
|
||||
- **Embed anything** - embed virtual browser in your web app - open source alternative to [hyperbeam API](https://hyperbeam.com/).
|
||||
- open any third-party website or application, synchronize audio and video flawlessly among multiple participants.
|
||||
- request rooms using API with [neko-rooms](https://github.com/m1k1o/neko-rooms).
|
||||
|
||||
Other use cases that benefit from single-user:
|
||||
- **Personal workspace** - streaming containerized apps and desktops to end-users - similar to [kasm](https://www.kasmweb.com/).
|
||||
- **Persistent browser** - own browser with persistent cookies available anywhere - similar to [mightyapp](https://www.mightyapp.com/).
|
||||
- no state is left on the host browser after terminating the connection.
|
||||
- sensitive data like cookies are not transferred - only video is shared.
|
||||
- **Throwaway browser** - a better solution for planning secret parties and buying birthday gifts off the internet.
|
||||
- use Tor Browser and [VPN](https://github.com/m1k1o/neko-vpn) for additional anonymity.
|
||||
- mitigates risk of OS fingerprinting and browser vulnerabilities by running in container.
|
||||
- **Session broadcasting** - broadcast room content using RTMP (to e.g. twitch or youtube...).
|
||||
- **Session recording** - broadcast RTMP can be saved to a file using e.g. [nginx-rtmp](https://www.nginx.com/products/nginx/modules/rtmp-media-streaming/)
|
||||
- have clean environment when recording tutorials.
|
||||
- no need to hide bookmarks or use incognito mode.
|
||||
- **Jump host** - access your internal applications securely without the need for VPN.
|
||||
- **Automated browser** - you can install [playwright](https://playwright.dev/) or [puppeteer](https://pptr.dev/) and automate tasks while being able to actively intercept them.
|
||||
|
||||
Compared to clientless remote desktop gateway (e.g. [Apache Guacamole](https://guacamole.apache.org/) or [websockify](https://github.com/novnc/websockify) with [noVNC](https://novnc.com/)), installed with remote desktop server along with desired program (e.g. [linuxserver/firefox](https://docs.linuxserver.io/images/docker-firefox)) provides neko additionally:
|
||||
- **Smooth video** because it uses WebRTC and not images sent over WebSockets.
|
||||
- **Built in audio** support, what is not part of Apache Guacamole or noVNC.
|
||||
- **Multi-participant control**, what is not natively supported by Apache Guacamole or noVNC.
|
||||
|
||||
### Supported browsers
|
||||
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/firefox.svg" title="m1k1o/neko:firefox" width="60" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/google-chrome.svg" title="m1k1o/neko:google-chrome" width="60" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/chromium.svg" title="m1k1o/neko:chromium" width="60" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/microsoft-edge.svg" title="m1k1o/neko:microsoft-edge" width="60" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/brave.svg" title="m1k1o/neko:brave" width="60" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vivaldi.svg" title="m1k1o/neko:vivaldi" width="60" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/opera.svg" title="m1k1o/neko:opera" width="60" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/tor-browser.svg" title="m1k1o/neko:tor-browser" width="60" height="auto"/>
|
||||
</div>
|
||||
|
||||
### Other programs
|
||||
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/remmina.png" title="m1k1o/neko:remmina" width="60" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vlc.svg" title="m1k1o/neko:vlc" width="60" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/xfce.svg" title="m1k1o/neko:xfce" width="60" height="auto"/>
|
||||
|
||||
... others in <a href="https://github.com/m1k1o/neko-apps">m1k1o/neko-apps</a>
|
||||
</div>
|
||||
|
||||
### Features
|
||||
|
||||
* Text Chat (With basic markdown support, discord flavor)
|
||||
* Admin users (Kick, Ban & Force Give/Release Controls)
|
||||
* Admin users (Kick, Ban & Force Give/Release Controls, Lock room)
|
||||
* Clipboard synchronization (on [supported browsers](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText))
|
||||
* Emote overlay
|
||||
* Ignore user (chat and emotes)
|
||||
* Persistent settings
|
||||
* Automatic Login with custom url args. (add `?usr=<your-user-name>&pwd=<room-pass>` to the url.)
|
||||
* Broadcasting room content using RTMP (to e.g. twitch or youtube...)
|
||||
* Bidirectional file transfer (if enabled)
|
||||
|
||||
<div align="center">
|
||||
|
||||
With `NEKO_FILE_TRANSFER_ENABLED=true`:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/file-transfer.gif" width="650" height="auto"/>
|
||||
</div>
|
||||
|
||||
### Why n.eko?
|
||||
|
||||
@ -42,13 +120,13 @@ I like cats 🐱 (`Neko` is the Japanese word for cat), I'm a weeb/nerd.
|
||||
|
||||
***But why the cat butt?*** Because cats are *assholes*, but you love them anyways.
|
||||
|
||||
# Multiple rooms
|
||||
## Multiple rooms
|
||||
|
||||
For n.eko room management software, visit [neko-rooms](https://github.com/m1k1o/neko-rooms).
|
||||
|
||||
It also offers zero-knowledge [installation script](https://github.com/m1k1o/neko-rooms/#zero-knowledge-installation).
|
||||
It also offers zero-knowledge [installation script (with HTTPS and Traefik)](https://github.com/m1k1o/neko-rooms/#zero-knowledge-installation-with-https-and-traefik).
|
||||
|
||||
# Documentation
|
||||
## Documentation
|
||||
|
||||
* [Getting Started](https://neko.m1k1o.net/#/getting-started/)
|
||||
* [Quick Start](https://neko.m1k1o.net/#/getting-started/quick-start)
|
||||
@ -62,6 +140,10 @@ It also offers zero-knowledge [installation script](https://github.com/m1k1o/nek
|
||||
* [Technologies](https://neko.m1k1o.net/#/technologies)
|
||||
* [Changelog](https://neko.m1k1o.net/#/changelog)
|
||||
|
||||
# How to contribute? How to build?
|
||||
## How to contribute? How to build?
|
||||
|
||||
Navigate to [.docker](.docker) folder for further information.
|
||||
|
||||
## Support
|
||||
|
||||
If you want to support this project, you can do it [here](https://github.com/sponsors/m1k1o).
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "neko-client",
|
||||
"version": "2.0.0",
|
||||
"version": "2.5.0",
|
||||
"description": "Client for neko streaming server",
|
||||
"license": "Apache License 2.0",
|
||||
"author": "Nurdism <https://github.com/nurdism>",
|
||||
@ -20,50 +20,50 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@fortawesome/fontawesome-free": "^6.2.0",
|
||||
"animejs": "^3.2.0",
|
||||
"axios": "^0.21.1",
|
||||
"date-fns": "^2.23.0",
|
||||
"axios": "^0.24.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"emoji-datasource": "^6.0.1",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"simple-markdown": "^0.7.2",
|
||||
"sweetalert2": "^10.15.7",
|
||||
"sweetalert2": "11.4.8",
|
||||
"typed-vuex": "^0.1.21",
|
||||
"v-tooltip": "^2.0.3",
|
||||
"vue": "^2.6.14",
|
||||
"vue": "^2.7.13",
|
||||
"vue-class-component": "^7.2.6",
|
||||
"vue-clickaway": "^2.2.2",
|
||||
"vue-context": "^5.2.0",
|
||||
"vue-i18n": "^8.25.0",
|
||||
"vue-i18n": "^8.27.2",
|
||||
"vue-notification": "^1.3.20",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/animejs": "^3.1.4",
|
||||
"@types/node": "^14.17.12",
|
||||
"@types/animejs": "^3.1.6",
|
||||
"@types/node": "^14.18.32",
|
||||
"@types/vue": "^2.0.0",
|
||||
"@types/vue-clickaway": "^2.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||
"@typescript-eslint/parser": "^4.30.0",
|
||||
"@vue/cli-plugin-babel": "^4.5.6",
|
||||
"@vue/cli-plugin-eslint": "^4.5.6",
|
||||
"@vue/cli-plugin-typescript": "^4.5.6",
|
||||
"@vue/cli-plugin-vuex": "^4.5.6",
|
||||
"@vue/cli-service": "^4.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"@vue/cli-plugin-babel": "^4.5.19",
|
||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
||||
"@vue/cli-plugin-typescript": "^4.5.19",
|
||||
"@vue/cli-plugin-vuex": "^4.5.19",
|
||||
"@vue/cli-service": "^4.5.19",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"@vue/eslint-config-typescript": "^7.0.0",
|
||||
"core-js": "^3.16.4",
|
||||
"emojilib": "^3.0.4",
|
||||
"core-js": "^3.26.0",
|
||||
"emojilib": "^3.0.7",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-vue": "^7.17.0",
|
||||
"node-sass": "^5.0.0",
|
||||
"prettier": "^2.3.2",
|
||||
"sass-loader": "^10.1.1",
|
||||
"eslint-plugin-vue": "^7.20.0",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.55.0",
|
||||
"sass-loader": "^10.3.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.4.2",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
"typescript": "^4.8.4",
|
||||
"vue-template-compiler": "^2.7.13"
|
||||
}
|
||||
}
|
||||
|
@ -181,10 +181,12 @@
|
||||
}
|
||||
|
||||
@Watch('hideControls', { immediate: true })
|
||||
onHideControls() {
|
||||
onHideControls(enabled: boolean) {
|
||||
if (enabled) {
|
||||
this.$accessor.video.setMuted(false)
|
||||
this.$accessor.settings.setSound(false)
|
||||
}
|
||||
}
|
||||
|
||||
controlAttempt() {
|
||||
if (this.shakeKbd || this.$accessor.remote.hosted) return
|
||||
|
@ -1,5 +1,7 @@
|
||||
// Variables
|
||||
|
||||
@use "sass:math";
|
||||
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
||||
$fa-font-size-base: 16px;
|
||||
$fa-font-display: auto;
|
||||
@ -7,11 +9,11 @@ $fa-css-prefix: fa;
|
||||
$fa-border-color: #eee;
|
||||
$fa-inverse: #fff;
|
||||
$fa-li-width: 2em;
|
||||
$fa-fw-width: (20em / 16);
|
||||
$fa-fw-width: math.div(20em, 16);
|
||||
$fa-primary-opacity: 1;
|
||||
$fa-secondary-opacity: .4;
|
||||
|
||||
$fa-family-default: 'Font Awesome 5 Free';
|
||||
$fa-family-default: 'Font Awesome 6 Free';
|
||||
|
||||
// Import FA source files
|
||||
@import "~@fortawesome/fontawesome-free/scss/brands";
|
||||
|
150
client/src/assets/styles/vendor/_swal.scss
vendored
@ -1,44 +1,16 @@
|
||||
$swal2-white: #fff;
|
||||
$swal2-black: #000;
|
||||
@import '~sweetalert2/src/variables';
|
||||
|
||||
$swal2-outline-color: transparent;
|
||||
|
||||
// CONTAINER
|
||||
$swal2-container-padding: .625em;
|
||||
|
||||
// BOX MODEL
|
||||
$swal2-width: 32em;
|
||||
// POPUP
|
||||
$swal2-padding: 1.25em;
|
||||
$swal2-border: none;
|
||||
$swal2-border-radius: .3125em;
|
||||
$swal2-box-shadow: #d9d9d9;
|
||||
|
||||
// ANIMATIONS
|
||||
$swal2-show-animation: swal2-show .3s;
|
||||
$swal2-hide-animation: swal2-hide .15s forwards;
|
||||
|
||||
// BACKGROUND
|
||||
$swal2-background: $background-secondary;
|
||||
|
||||
// TYPOGRAPHY
|
||||
$swal2-font: inherit;
|
||||
$swal2-font-size: 1rem;
|
||||
|
||||
// BACKDROP
|
||||
$swal2-backdrop: rgba($swal2-black, .4);
|
||||
$swal2-backdrop-transition: background-color .1s;
|
||||
|
||||
// ICONS
|
||||
$swal2-icon-size: 5em;
|
||||
$swal2-icon-animations: true;
|
||||
$swal2-icon-margin: 1.25em auto 1.875em;
|
||||
$swal2-icon-zoom: null;
|
||||
$swal2-success: #a5dc86;
|
||||
$swal2-success-border: rgba($swal2-success, .3);
|
||||
$swal2-error: #f27474;
|
||||
$swal2-warning: #f8bb86;
|
||||
$swal2-info: #3fc3ee;
|
||||
$swal2-question: #87adbd;
|
||||
$swal2-icon-font-family: inherit;
|
||||
|
||||
// IMAGE
|
||||
$swal2-image-margin: 1.25em auto;
|
||||
@ -46,143 +18,53 @@ $swal2-image-margin: 1.25em auto;
|
||||
// TITLE
|
||||
$swal2-title-margin: 0 0 .4em;
|
||||
$swal2-title-color: $interactive-hover;
|
||||
$swal2-title-font-size: 1.875em;
|
||||
|
||||
// CONTENT
|
||||
$swal2-content-justify-content: center;
|
||||
$swal2-content-margin: 0;
|
||||
$swal2-content-pading: 0;
|
||||
$swal2-content-color: $interactive-hover;
|
||||
$swal2-content-font-size: 1.125em;
|
||||
$swal2-content-font-weight: normal;
|
||||
$swal2-content-line-height: normal;
|
||||
$swal2-content-text-align: center;
|
||||
$swal2-content-word-wrap: break-word;
|
||||
// HTML CONTAINER
|
||||
$swal2-html-container-margin: 0;
|
||||
$swal2-html-container-color: $interactive-hover;
|
||||
|
||||
// INPUT
|
||||
$swal2-input-margin: 1em auto;
|
||||
$swal2-input-width: 100%;
|
||||
$swal2-input-height: 2.625em;
|
||||
$swal2-input-padding: 0 .75em;
|
||||
$swal2-input-border: 1px solid lighten($swal2-black, 85);
|
||||
$swal2-input-border-radius: .1875em;
|
||||
$swal2-input-box-shadow: inset 0 1px 1px rgba($swal2-black, .06);
|
||||
$swal2-input-focus-border: 1px solid #b4dbed;
|
||||
$swal2-input-focus-outline: none;
|
||||
$swal2-input-focus-box-shadow: 0 0 3px #c4e6f5;
|
||||
$swal2-input-font-size: 1.125em;
|
||||
$swal2-input-background: inherit;
|
||||
$swal2-input-color: inherit;
|
||||
$swal2-input-transition: border-color .3s, box-shadow .3s;
|
||||
|
||||
// TEXTAREA SPECIFIC VARIABLES
|
||||
$swal2-textarea-height: 6.75em;
|
||||
$swal2-textarea-padding: .75em;
|
||||
|
||||
// VALIDATION MESSAGE
|
||||
$swal2-validation-message-justify-content: center;
|
||||
$swal2-validation-message-padding: .625em;
|
||||
$swal2-validation-message-background: lighten($swal2-black, 94);
|
||||
$swal2-validation-message-color: lighten($swal2-black, 40);
|
||||
$swal2-validation-message-font-size: 1em;
|
||||
$swal2-validation-message-font-weight: 300;
|
||||
$swal2-validation-message-icon-background: $swal2-error;
|
||||
$swal2-validation-message-icon-color: $swal2-white;
|
||||
$swal2-validation-message-icon-zoom: null;
|
||||
|
||||
// PROGRESS STEPS
|
||||
$swal2-progress-steps-background: inherit;
|
||||
$swal2-progress-steps-margin: 0 0 1.25em;
|
||||
$swal2-progress-steps-padding: 0;
|
||||
$swal2-progress-steps-font-weight: 600;
|
||||
$swal2-progress-steps-distance: 2.5em;
|
||||
$swal2-progress-step-width: 2em;
|
||||
$swal2-progress-step-height: 2em;
|
||||
$swal2-progress-step-border-radius: 2em;
|
||||
$swal2-progress-step-background: #add8e6;
|
||||
$swal2-progress-step-color: $swal2-white;
|
||||
$swal2-active-step-background: #3085d6;
|
||||
$swal2-active-step-color: $swal2-white;
|
||||
|
||||
// FOOTER
|
||||
$swal2-footer-margin: 1.25em 0 0;
|
||||
$swal2-footer-padding: 1em 0 0;
|
||||
$swal2-footer-border-color: #eee;
|
||||
$swal2-footer-color: lighten($swal2-black, 33);
|
||||
$swal2-footer-font-size: 1em;
|
||||
|
||||
// TIMER POGRESS BAR
|
||||
$swal2-timer-progress-bar-height: .25em;
|
||||
$swal2-timer-progress-bar-background: rgba($swal2-black, .2);
|
||||
|
||||
// CLOSE BUTTON
|
||||
$swal2-close-button-width: 1.2em;
|
||||
$swal2-close-button-height: 1.2em;
|
||||
$swal2-close-button-line-height: 1.2;
|
||||
$swal2-close-button-position: absolute;
|
||||
$swal2-close-button-gap: 0;
|
||||
$swal2-close-button-transition: color .1s ease-out;
|
||||
$swal2-close-button-border: none;
|
||||
$swal2-close-button-border-radius: 0;
|
||||
$swal2-close-button-outline: initial;
|
||||
$swal2-close-button-background: transparent;
|
||||
$swal2-close-button-color: lighten($swal2-black, 80);
|
||||
$swal2-close-button-font-family: serif;
|
||||
$swal2-close-button-font-size: 2.5em;
|
||||
|
||||
// CLOSE BUTTON:HOVER
|
||||
$swal2-close-button-hover-transform: none;
|
||||
$swal2-close-button-hover-color: $swal2-error;
|
||||
$swal2-close-button-hover-background: transparent;
|
||||
|
||||
// ACTIONS
|
||||
$swal2-actions-flex-wrap: wrap;
|
||||
$swal2-actions-align-items: center;
|
||||
$swal2-actions-justify-content: center;
|
||||
$swal2-actions-width: 100%;
|
||||
$swal2-actions-margin: 1.25em auto 0;
|
||||
|
||||
// CONFIRM BUTTON
|
||||
$swal2-confirm-button-border: 0;
|
||||
$swal2-confirm-button-border-radius: .25em;
|
||||
$swal2-confirm-button-background-color: $background-tertiary;
|
||||
$swal2-confirm-button-color: $swal2-white;
|
||||
$swal2-confirm-button-font-size: 1.0625em;
|
||||
|
||||
// CANCEL BUTTON
|
||||
$swal2-cancel-button-border: 0;
|
||||
$swal2-cancel-button-border-radius: .25em;
|
||||
$swal2-cancel-button-background-color: $background-floating;
|
||||
$swal2-cancel-button-color: $swal2-white;
|
||||
$swal2-cancel-button-font-size: 1.0625em;
|
||||
|
||||
// COMMON VARIABLES FOR CONFIRM AND CANCEL BUTTONS
|
||||
$swal2-button-darken-hover: rgba($swal2-black, .1);
|
||||
$swal2-button-darken-active: rgba($swal2-black, .2);
|
||||
$swal2-button-focus-outline: none;
|
||||
$swal2-button-focus-background-color: null;
|
||||
$swal2-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
|
||||
|
||||
// CONFIRM BUTTON
|
||||
$swal2-confirm-button-background-color: $background-tertiary;
|
||||
$swal2-confirm-button-font-size: 1.0625em;
|
||||
$swal2-confirm-button-focus-box-shadow: 0 0 0 3px rgba($swal2-confirm-button-background-color, .5);
|
||||
|
||||
// CANCEL BUTTON
|
||||
$swal2-cancel-button-background-color: $background-floating;
|
||||
$swal2-cancel-button-font-size: 1.0625em;
|
||||
$swal2-cancel-button-focus-box-shadow: 0 0 0 3px rgba($swal2-cancel-button-background-color, .5);
|
||||
|
||||
// TOASTS
|
||||
$swal2-toast-show-animation: swal2-toast-show .5s;
|
||||
$swal2-toast-hide-animation: swal2-toast-hide .1s forwards;
|
||||
$swal2-toast-border: none;
|
||||
$swal2-toast-box-shadow: 0 0 .625em #d9d9d9;
|
||||
$swal2-toast-background: $swal2-white;
|
||||
$swal2-toast-close-button-width: .8em;
|
||||
$swal2-toast-close-button-height: .8em;
|
||||
$swal2-toast-close-button-line-height: .8;
|
||||
$swal2-toast-width: auto;
|
||||
$swal2-toast-padding: .625em;
|
||||
$swal2-toast-title-margin: 0 .6em;
|
||||
$swal2-toast-title-font-size: 1em;
|
||||
$swal2-toast-content-font-size: 1em;
|
||||
$swal2-toast-input-font-size: 1em;
|
||||
$swal2-toast-validation-font-size: 1em;
|
||||
$swal2-toast-buttons-font-size: 1em;
|
||||
$swal2-toast-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
|
||||
$swal2-toast-footer-margin: .5em 0 0;
|
||||
$swal2-toast-footer-padding: .5em 0 0;
|
||||
$swal2-toast-footer-font-size: .8em;
|
||||
|
||||
@import "~sweetalert2/src/sweetalert2.scss";
|
@ -18,20 +18,20 @@
|
||||
<span @click="mute(child.data.member)" v-if="!child.data.member.muted">{{ $t('context.mute') }}</span>
|
||||
<span @click="unmute(child.data.member)" v-else>{{ $t('context.unmute') }}</span>
|
||||
</li>
|
||||
<li v-if="child.data.member.id === host">
|
||||
<li v-if="child.data.member.id === host && !implicitHosting">
|
||||
<span @click="adminRelease(child.data.member)">{{ $t('context.release') }}</span>
|
||||
</li>
|
||||
<li v-if="child.data.member.id === host">
|
||||
<li v-if="child.data.member.id === host && !implicitHosting">
|
||||
<span @click="adminControl(child.data.member)">{{ $t('context.take') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span v-if="child.data.member.id !== host" @click="adminGive(child.data.member)">{{
|
||||
<span v-if="child.data.member.id !== host && !implicitHosting" @click="adminGive(child.data.member)">{{
|
||||
$t('context.give')
|
||||
}}</span>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li v-if="hosting">
|
||||
<li v-if="hosting && !implicitHosting">
|
||||
<span @click="give(child.data.member)">{{ $t('context.give') }}</span>
|
||||
</li>
|
||||
</template>
|
||||
@ -161,6 +161,10 @@
|
||||
return this.$accessor.remote.id
|
||||
}
|
||||
|
||||
get implicitHosting() {
|
||||
return this.$accessor.remote.implicitHosting
|
||||
}
|
||||
|
||||
open(event: MouseEvent, data: any) {
|
||||
this.context.open(event, data)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ul>
|
||||
<li v-if="!isTouch && seesControl">
|
||||
<li v-if="!implicitHosting && (!controlLocked || hosting)">
|
||||
<i
|
||||
:class="[
|
||||
!disabeld && shakeKbd ? 'shake' : '',
|
||||
@ -20,7 +20,19 @@
|
||||
@click.stop.prevent="toggleControl"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="seesControl">
|
||||
<li class="no-pointer" v-if="implicitHosting">
|
||||
<i
|
||||
:class="[controlLocked ? 'disabled' : '', 'fas', 'fa-mouse-pointer']"
|
||||
v-tooltip="{
|
||||
content: controlLocked ? $t('controls.hasnot') : $t('controls.has'),
|
||||
placement: 'top',
|
||||
offset: 5,
|
||||
boundariesElement: 'body',
|
||||
delay: { show: 300, hide: 100 },
|
||||
}"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="implicitHosting || (!implicitHosting && (!controlLocked || hosting))">
|
||||
<label
|
||||
class="switch"
|
||||
v-tooltip="{
|
||||
@ -31,7 +43,7 @@
|
||||
delay: { show: 300, hide: 100 },
|
||||
}"
|
||||
>
|
||||
<input type="checkbox" v-model="locked" :disabled="!hosting" />
|
||||
<input type="checkbox" v-model="locked" :disabled="!hosting || (implicitHosting && controlLocked)" />
|
||||
<span />
|
||||
</label>
|
||||
</li>
|
||||
@ -105,6 +117,10 @@
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
&.no-pointer {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
i {
|
||||
padding: 0 5px;
|
||||
|
||||
@ -196,7 +212,7 @@
|
||||
&:before {
|
||||
color: $background-tertiary;
|
||||
font-weight: 900;
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
content: '\f3c1';
|
||||
font-size: 8px;
|
||||
line-height: 18px;
|
||||
@ -242,19 +258,8 @@
|
||||
export default class extends Vue {
|
||||
@Prop(Boolean) readonly shakeKbd!: boolean
|
||||
|
||||
get isTouch() {
|
||||
return (
|
||||
(typeof navigator.maxTouchPoints !== 'undefined' ? navigator.maxTouchPoints < 0 : false) ||
|
||||
'ontouchstart' in document.documentElement
|
||||
)
|
||||
}
|
||||
|
||||
get severLocked(): boolean {
|
||||
return 'control' in this.$accessor.locked && this.$accessor.locked['control']
|
||||
}
|
||||
|
||||
get seesControl(): boolean {
|
||||
return !this.severLocked || this.$accessor.user.admin || this.hosting
|
||||
get controlLocked() {
|
||||
return 'control' in this.$accessor.locked && this.$accessor.locked['control'] && !this.$accessor.user.admin
|
||||
}
|
||||
|
||||
get disabeld() {
|
||||
@ -265,6 +270,10 @@
|
||||
return this.$accessor.remote.hosting
|
||||
}
|
||||
|
||||
get implicitHosting() {
|
||||
return this.$accessor.remote.implicitHosting
|
||||
}
|
||||
|
||||
get volume() {
|
||||
return this.$accessor.video.volume
|
||||
}
|
||||
|
@ -90,7 +90,7 @@
|
||||
&::before {
|
||||
content: '\f002';
|
||||
font-weight: 900;
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
position: absolute;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
520
client/src/components/files.vue
Normal file
@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<div class="files">
|
||||
<div class="files-cwd">
|
||||
<p>{{ cwd }}</p>
|
||||
<i class="fas fa-rotate-right refresh" @click="refresh" />
|
||||
</div>
|
||||
<div class="files-list">
|
||||
<div v-for="item in files" :key="item.name" class="files-list-item">
|
||||
<i :class="fileIcon(item)" />
|
||||
<p class="file-name" :title="item.name">{{ item.name }}</p>
|
||||
<p class="file-size">{{ fileSize(item.size) }}</p>
|
||||
<i v-if="item.type !== 'dir'" class="fas fa-download download" @click="download(item)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="transfer-area">
|
||||
<div class="transfers" v-if="transfers.length > 0">
|
||||
<p v-if="downloads.length > 0" class="transfers-list-header">
|
||||
<span>{{ $t('files.downloads') }}</span>
|
||||
<i class="fas fa-xmark remove-transfer" @click="downloads.forEach((t) => removeTransfer(t))"></i>
|
||||
</p>
|
||||
<div v-for="download in downloads" :key="download.id" class="transfers-list-item">
|
||||
<div class="transfer-info">
|
||||
<i
|
||||
class="fas transfer-status"
|
||||
:class="{
|
||||
'fa-clock': download.status === 'pending',
|
||||
'fa-arrows-rotate': download.status === 'inprogress',
|
||||
'fa-check': download.status === 'completed',
|
||||
'fa-warning': download.status === 'failed',
|
||||
}"
|
||||
></i>
|
||||
<p class="file-name" :title="download.name">{{ download.name }}</p>
|
||||
<p class="file-size">{{ Math.min(100, Math.round((download.progress / download.size) * 100)) }}%</p>
|
||||
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(download)"></i>
|
||||
</div>
|
||||
<div v-if="download.status === 'failed'" class="transfer-error">{{ download.error }}</div>
|
||||
<progress
|
||||
v-else
|
||||
class="transfer-progress"
|
||||
:aria-label="download.name + ' progress'"
|
||||
:value="download.progress"
|
||||
:max="download.size"
|
||||
></progress>
|
||||
</div>
|
||||
<p v-if="uploads.length > 0" class="transfers-list-header">
|
||||
<span>{{ $t('files.uploads') }}</span>
|
||||
<i class="fas fa-xmark remove-transfer" @click="uploads.forEach((t) => removeTransfer(t))"></i>
|
||||
</p>
|
||||
<div v-for="upload in uploads" :key="upload.id" class="transfers-list-item">
|
||||
<div class="transfer-info">
|
||||
<i
|
||||
class="fas transfer-status"
|
||||
:title="upload.status"
|
||||
:class="{
|
||||
'fa-clock': upload.status === 'pending',
|
||||
'fa-arrows-rotate': upload.status === 'inprogress',
|
||||
'fa-check': upload.status === 'completed',
|
||||
'fa-warning': upload.status === 'failed',
|
||||
}"
|
||||
></i>
|
||||
<p class="file-name" :title="upload.name">{{ upload.name }}</p>
|
||||
<p class="file-size">{{ Math.min(100, Math.round((upload.progress / upload.size) * 100)) }}%</p>
|
||||
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(upload)"></i>
|
||||
</div>
|
||||
<div v-if="upload.status === 'failed'" class="transfer-error">{{ upload.error }}</div>
|
||||
<progress
|
||||
v-else
|
||||
class="transfer-progress"
|
||||
:aria-label="upload.name + ' progress'"
|
||||
:value="upload.progress"
|
||||
:max="upload.size"
|
||||
></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="upload-area"
|
||||
:class="{ 'upload-area-drag': uploadAreaDrag }"
|
||||
@dragover.prevent="uploadAreaDrag = true"
|
||||
@dragleave.prevent="uploadAreaDrag = false"
|
||||
@drop.prevent="(e) => upload(e.dataTransfer)"
|
||||
@click="openFileBrowser"
|
||||
>
|
||||
<i class="fas fa-file-arrow-up" />
|
||||
<p>{{ $t('files.upload_here') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.files {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
|
||||
.files-cwd {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 10px 10px 0px 10px;
|
||||
padding: 0.5em;
|
||||
font-weight: 600;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.files-list {
|
||||
margin: 10px 10px 10px 10px;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 5px;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $background-tertiary transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $background-tertiary;
|
||||
border: 2px solid $background-primary;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: $background-floating;
|
||||
}
|
||||
}
|
||||
|
||||
.files-list-item {
|
||||
padding: 0.5em;
|
||||
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.transfers-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
|
||||
}
|
||||
|
||||
.file-icon,
|
||||
.transfer-status {
|
||||
width: 14px;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.transfer-error {
|
||||
border: 1px solid $style-error;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.files-list-item:last-child {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.refresh {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
margin-left: auto;
|
||||
margin-right: 0.5em;
|
||||
color: rgba($color: #fff, $alpha: 0.4);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.refresh:hover,
|
||||
.download:hover,
|
||||
.remove-transfer:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.transfer-area {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.transfers {
|
||||
margin: 10px 10px 10px 10px;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 5px;
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $background-tertiary transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $background-tertiary;
|
||||
border: 2px solid $background-primary;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: $background-floating;
|
||||
}
|
||||
}
|
||||
|
||||
.transfers > p {
|
||||
padding: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.transfer-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.transfer-progress {
|
||||
margin: 0px 10px 10px 10px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
margin: 10px 10px 10px 10px;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-area-drag,
|
||||
.upload-area:hover {
|
||||
background-color: rgba($color: #fff, $alpha: 0.1);
|
||||
}
|
||||
|
||||
.upload-area > i {
|
||||
font-size: 4em;
|
||||
margin: 10px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.upload-area > p {
|
||||
margin: 0px 10px 10px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
|
||||
import Markdown from './markdown'
|
||||
import Content from './context.vue'
|
||||
import { FileTransfer, FileListItem } from '~/neko/types'
|
||||
|
||||
@Component({
|
||||
name: 'neko-files',
|
||||
components: {
|
||||
'neko-markdown': Markdown,
|
||||
'neko-context': Content,
|
||||
},
|
||||
})
|
||||
export default class extends Vue {
|
||||
public uploadAreaDrag: boolean = false
|
||||
|
||||
get cwd() {
|
||||
return this.$accessor.files.cwd
|
||||
}
|
||||
|
||||
get files() {
|
||||
return this.$accessor.files.files
|
||||
}
|
||||
|
||||
get transfers() {
|
||||
return this.$accessor.files.transfers
|
||||
}
|
||||
|
||||
get downloads() {
|
||||
return this.$accessor.files.transfers.filter((t) => t.direction === 'download')
|
||||
}
|
||||
|
||||
get uploads() {
|
||||
return this.$accessor.files.transfers.filter((t) => t.direction === 'upload')
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.$accessor.files.refresh()
|
||||
}
|
||||
|
||||
download(item: FileListItem) {
|
||||
if (this.downloads.map((t) => t.name).includes(item.name)) {
|
||||
return
|
||||
}
|
||||
|
||||
const url =
|
||||
'/file?pwd=' + encodeURIComponent(this.$accessor.password) + '&filename=' + encodeURIComponent(item.name)
|
||||
const abortController = new AbortController()
|
||||
|
||||
let transfer: FileTransfer = {
|
||||
id: Math.round(Math.random() * 10000),
|
||||
name: item.name,
|
||||
direction: 'download',
|
||||
// this may be smaller than the actual transfer amount, but for large files the
|
||||
// content length is not sent (chunked transfer)
|
||||
size: item.size,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
abortController: abortController,
|
||||
}
|
||||
|
||||
this.$http
|
||||
.get(url, {
|
||||
responseType: 'blob',
|
||||
signal: abortController.signal,
|
||||
withCredentials: false,
|
||||
onDownloadProgress: (x) => {
|
||||
transfer.progress = x.loaded
|
||||
|
||||
if (x.lengthComputable && transfer.size !== x.total) {
|
||||
transfer.size = x.total
|
||||
}
|
||||
if (transfer.progress === transfer.size) {
|
||||
transfer.status = 'completed'
|
||||
} else if (transfer.status !== 'inprogress') {
|
||||
transfer.status = 'inprogress'
|
||||
}
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
const url = window.URL.createObjectURL(new Blob([res.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', item.name)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
transfer.progress = transfer.size
|
||||
transfer.status = 'completed'
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$log.error(error)
|
||||
|
||||
transfer.status = 'failed'
|
||||
transfer.error = error.message
|
||||
})
|
||||
|
||||
this.$accessor.files.addTransfer(transfer)
|
||||
}
|
||||
|
||||
upload(dt: DataTransfer) {
|
||||
const url = '/file?pwd=' + encodeURIComponent(this.$accessor.password)
|
||||
this.uploadAreaDrag = false
|
||||
|
||||
for (const file of dt.files) {
|
||||
const abortController = new AbortController()
|
||||
|
||||
const formdata = new FormData()
|
||||
formdata.append('files', file, file.name)
|
||||
|
||||
let transfer: FileTransfer = {
|
||||
id: Math.round(Math.random() * 10000),
|
||||
name: file.name,
|
||||
direction: 'upload',
|
||||
size: file.size,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
abortController: abortController,
|
||||
}
|
||||
|
||||
this.$http
|
||||
.post(url, formdata, {
|
||||
signal: abortController.signal,
|
||||
withCredentials: false,
|
||||
onUploadProgress: (x: any) => {
|
||||
transfer.progress = x.loaded
|
||||
|
||||
if (transfer.size !== x.total) {
|
||||
transfer.size = x.total
|
||||
}
|
||||
if (transfer.progress === transfer.size) {
|
||||
transfer.status = 'completed'
|
||||
} else if (transfer.status !== 'inprogress') {
|
||||
transfer.status = 'inprogress'
|
||||
}
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$log.error(error)
|
||||
|
||||
transfer.status = 'failed'
|
||||
transfer.error = error.message
|
||||
})
|
||||
|
||||
this.$accessor.files.addTransfer(transfer)
|
||||
}
|
||||
}
|
||||
|
||||
openFileBrowser() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.setAttribute('multiple', 'true')
|
||||
input.onchange = (e: Event) => {
|
||||
if (e === null) return
|
||||
|
||||
const dt = new DataTransfer()
|
||||
const target = e.target as HTMLInputElement
|
||||
if (target.files === null) return
|
||||
|
||||
for (const f of target.files) {
|
||||
dt.items.add(f)
|
||||
}
|
||||
|
||||
this.upload(dt)
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
removeTransfer(transfer: FileTransfer) {
|
||||
if (transfer.status !== 'completed') {
|
||||
transfer.abortController?.abort()
|
||||
}
|
||||
this.$accessor.files.removeTransfer(transfer)
|
||||
}
|
||||
|
||||
fileIcon(file: FileListItem) {
|
||||
let className = 'file-icon fas '
|
||||
// if is directory
|
||||
if (file.type === 'dir') {
|
||||
className += 'fa-folder'
|
||||
return className
|
||||
}
|
||||
// try to get file extension
|
||||
const ext = file.name.split('.').pop()
|
||||
if (ext === undefined) {
|
||||
className += 'fa-file'
|
||||
return className
|
||||
}
|
||||
// try to find icon
|
||||
switch (ext.toLowerCase()) {
|
||||
case 'txt':
|
||||
case 'md':
|
||||
className += 'fa-file-text'
|
||||
break
|
||||
case 'pdf':
|
||||
className += 'fa-file-pdf'
|
||||
break
|
||||
case 'zip':
|
||||
case 'rar':
|
||||
case '7z':
|
||||
case 'gz':
|
||||
className += 'fa-archive'
|
||||
break
|
||||
case 'aac':
|
||||
case 'flac':
|
||||
case 'midi':
|
||||
case 'mp3':
|
||||
case 'ogg':
|
||||
case 'wav':
|
||||
className += 'fa-music'
|
||||
break
|
||||
case 'avi':
|
||||
case 'mkv':
|
||||
case 'mov':
|
||||
case 'mpeg':
|
||||
case 'mp4':
|
||||
case 'webm':
|
||||
className += 'fa-film'
|
||||
break
|
||||
case 'bmp':
|
||||
case 'gif':
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
case 'png':
|
||||
case 'svg':
|
||||
case 'tiff':
|
||||
case 'webp':
|
||||
className += 'fa-image'
|
||||
break
|
||||
default:
|
||||
className += 'fa-file'
|
||||
}
|
||||
return className
|
||||
}
|
||||
|
||||
fileSize(size: number) {
|
||||
if (size < 1024) {
|
||||
return size + ' B'
|
||||
}
|
||||
if (size < 1024 * 1024) {
|
||||
return Math.round(size / 1024) + ' KB'
|
||||
}
|
||||
if (size < 1024 * 1024 * 1024) {
|
||||
return Math.round(size / (1024 * 1024)) + ' MB'
|
||||
}
|
||||
if (size < 1024 * 1024 * 1024 * 1024) {
|
||||
return Math.round(size / (1024 * 1024 * 1024)) + ' GB'
|
||||
}
|
||||
return Math.round(size / (1024 * 1024 * 1024 * 1024)) + ' TB'
|
||||
}
|
||||
}
|
||||
</script>
|
@ -31,6 +31,19 @@
|
||||
}"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="fileTransfer">
|
||||
<i
|
||||
:class="[{ disabled: !admin }, { locked: isLocked('file_transfer') }, 'fas', 'fa-file']"
|
||||
@click="toggleLock('file_transfer')"
|
||||
v-tooltip="{
|
||||
content: lockedTooltip('file_transfer'),
|
||||
placement: 'bottom',
|
||||
offset: 5,
|
||||
boundariesElement: 'body',
|
||||
delay: { show: 300, hide: 100 },
|
||||
}"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span v-if="showBadge" class="badge">•</span>
|
||||
<i class="fas fa-bars toggle" @click="toggleMenu" />
|
||||
@ -169,26 +182,24 @@
|
||||
return !this.side && this.readTexts != this.texts
|
||||
}
|
||||
|
||||
get fileTransfer() {
|
||||
return this.$accessor.remote.fileTransfer
|
||||
}
|
||||
|
||||
toggleLock(resource: AdminLockResource) {
|
||||
this.$accessor.toggleLock(resource)
|
||||
}
|
||||
|
||||
isLocked(resource: AdminLockResource): boolean {
|
||||
return this.$accessor.isLocked(resource)
|
||||
}
|
||||
|
||||
readTexts: number = 0
|
||||
toggleMenu() {
|
||||
this.$accessor.client.toggleSide()
|
||||
this.readTexts = this.texts
|
||||
}
|
||||
|
||||
toggleLock(resource: AdminLockResource) {
|
||||
if (!this.admin) return
|
||||
|
||||
if (this.isLocked(resource)) {
|
||||
this.$accessor.unlock(resource)
|
||||
} else {
|
||||
this.$accessor.lock(resource)
|
||||
}
|
||||
}
|
||||
|
||||
isLocked(resource: AdminLockResource): boolean {
|
||||
return resource in this.locked && this.locked[resource]
|
||||
}
|
||||
|
||||
lockedTooltip(resource: AdminLockResource) {
|
||||
if (this.admin) {
|
||||
return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `unlock` : `lock`))
|
||||
|
@ -201,7 +201,7 @@ const rules: MarkdownRules = {
|
||||
},
|
||||
emoji: {
|
||||
order: md.defaultRules.strong.order,
|
||||
match: (source) => /^:([a-zA-z_-]*):/.exec(source),
|
||||
match: (source) => /^:([^:\s]+):/.exec(source),
|
||||
parse(capture) {
|
||||
return {
|
||||
id: capture[1],
|
||||
|
@ -77,7 +77,7 @@
|
||||
|
||||
&.self {
|
||||
&::before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-weight: 900;
|
||||
content: '\f2bd';
|
||||
background: $background-floating;
|
||||
@ -97,7 +97,7 @@
|
||||
&.admin {
|
||||
&::before {
|
||||
display: block;
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-weight: 900;
|
||||
content: '\f3ed';
|
||||
color: $style-primary;
|
||||
@ -114,7 +114,7 @@
|
||||
|
||||
&.host::after {
|
||||
display: block;
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-weight: 900;
|
||||
content: '\f521';
|
||||
background: $style-primary;
|
||||
|
@ -110,7 +110,7 @@
|
||||
},
|
||||
})
|
||||
export default class extends Vue {
|
||||
@Ref('context') readonly context!: any
|
||||
@Ref('context') readonly context!: VueContext
|
||||
|
||||
get width() {
|
||||
return this.$accessor.video.width
|
||||
|
@ -6,6 +6,10 @@
|
||||
<i class="fas fa-comment-alt" />
|
||||
<span>{{ $t('side.chat') }}</span>
|
||||
</li>
|
||||
<li v-if="filetransferAllowed" :class="{ active: tab === 'files' }" @click.stop.prevent="change('files')">
|
||||
<i class="fas fa-file" />
|
||||
<span>{{ $t('side.files') }}</span>
|
||||
</li>
|
||||
<li :class="{ active: tab === 'settings' }" @click.stop.prevent="change('settings')">
|
||||
<i class="fas fa-sliders-h" />
|
||||
<span>{{ $t('side.settings') }}</span>
|
||||
@ -14,6 +18,7 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<neko-chat v-if="tab === 'chat'" />
|
||||
<neko-files v-if="tab === 'files'" />
|
||||
<neko-settings v-if="tab === 'settings'" />
|
||||
</div>
|
||||
</aside>
|
||||
@ -74,23 +79,47 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'vue-property-decorator'
|
||||
import { Vue, Component, Watch } from 'vue-property-decorator'
|
||||
|
||||
import Settings from '~/components/settings.vue'
|
||||
import Chat from '~/components/chat.vue'
|
||||
import Files from '~/components/files.vue'
|
||||
|
||||
@Component({
|
||||
name: 'neko',
|
||||
components: {
|
||||
'neko-settings': Settings,
|
||||
'neko-chat': Chat,
|
||||
'neko-files': Files,
|
||||
},
|
||||
})
|
||||
export default class extends Vue {
|
||||
get filetransferAllowed() {
|
||||
return (
|
||||
this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer'))
|
||||
)
|
||||
}
|
||||
|
||||
get tab() {
|
||||
return this.$accessor.client.tab
|
||||
}
|
||||
|
||||
@Watch('tab', { immediate: true })
|
||||
@Watch('filetransferAllowed', { immediate: true })
|
||||
onTabChange() {
|
||||
// do not show the files tab if file transfer is disabled
|
||||
if (this.tab === 'files' && !this.filetransferAllowed) {
|
||||
this.change('chat')
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('filetransferAllowed')
|
||||
onFileTransferAllowedChange() {
|
||||
if (this.filetransferAllowed) {
|
||||
this.$accessor.files.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
change(tab: string) {
|
||||
this.$accessor.client.setTab(tab)
|
||||
}
|
||||
|
@ -8,10 +8,11 @@
|
||||
<neko-emote :id="index" :key="index" />
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
<textarea
|
||||
ref="overlay"
|
||||
class="overlay"
|
||||
tabindex="0"
|
||||
data-gramm="false"
|
||||
@click.stop.prevent
|
||||
@contextmenu.stop.prevent
|
||||
@wheel.stop.prevent="onWheel"
|
||||
@ -31,7 +32,7 @@
|
||||
</div>
|
||||
<ul v-if="!fullscreen && !hideControls" class="video-menu top">
|
||||
<li><i @click.stop.prevent="requestFullscreen" class="fas fa-expand"></i></li>
|
||||
<li v-if="admin"><i @click.stop.prevent="onResolution" class="fas fa-desktop"></i></li>
|
||||
<li v-if="admin"><i @click.stop.prevent="openResolution" class="fas fa-desktop"></i></li>
|
||||
<li class="request-control">
|
||||
<i
|
||||
:class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']"
|
||||
@ -41,7 +42,7 @@
|
||||
</ul>
|
||||
<ul v-if="!fullscreen && !hideControls" class="video-menu bottom">
|
||||
<li v-if="hosting && (!clipboard_read_available || !clipboard_write_available)">
|
||||
<i @click.stop.prevent="onClipboard" class="fas fa-clipboard"></i>
|
||||
<i @click.stop.prevent="openClipboard" class="fas fa-clipboard"></i>
|
||||
</li>
|
||||
<li>
|
||||
<i
|
||||
@ -68,6 +69,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #000;
|
||||
|
||||
.video-menu {
|
||||
position: absolute;
|
||||
@ -123,7 +125,7 @@
|
||||
.player-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 16 / 9 * 100vh;
|
||||
max-width: calc(16 / 9 * 100vh);
|
||||
|
||||
video {
|
||||
position: absolute;
|
||||
@ -172,6 +174,12 @@
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
color: transparent;
|
||||
background: transparent;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.player-aspect {
|
||||
@ -186,6 +194,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue, Prop } from 'vue-property-decorator'
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
import { elementRequestFullscreen, onFullscreenChange, isFullscreen } from '~/utils'
|
||||
|
||||
import Emote from './emote.vue'
|
||||
import Resolution from './resolution.vue'
|
||||
@ -207,17 +216,17 @@
|
||||
export default class extends Vue {
|
||||
@Ref('component') readonly _component!: HTMLElement
|
||||
@Ref('container') readonly _container!: HTMLElement
|
||||
@Ref('overlay') readonly _overlay!: HTMLElement
|
||||
@Ref('overlay') readonly _overlay!: HTMLTextAreaElement
|
||||
@Ref('aspect') readonly _aspect!: HTMLElement
|
||||
@Ref('player') readonly _player!: HTMLElement
|
||||
@Ref('video') readonly _video!: HTMLVideoElement
|
||||
@Ref('resolution') readonly _resolution!: any
|
||||
@Ref('clipboard') readonly _clipboard!: any
|
||||
@Ref('resolution') readonly _resolution!: Resolution
|
||||
@Ref('clipboard') readonly _clipboard!: Clipboard
|
||||
|
||||
@Prop(Boolean) readonly hideControls!: boolean
|
||||
|
||||
private keyboard = GuacamoleKeyboard()
|
||||
private observer = new ResizeObserver(this.onResise.bind(this))
|
||||
private observer = new ResizeObserver(this.onResize.bind(this))
|
||||
private focused = false
|
||||
private fullscreen = false
|
||||
private startsMuted = true
|
||||
@ -239,6 +248,10 @@
|
||||
return this.$accessor.remote.hosting
|
||||
}
|
||||
|
||||
get implicitHosting() {
|
||||
return this.$accessor.remote.implicitHosting
|
||||
}
|
||||
|
||||
get hosted() {
|
||||
return this.$accessor.remote.hosted
|
||||
}
|
||||
@ -271,8 +284,13 @@
|
||||
return this.$accessor.settings.autoplay
|
||||
}
|
||||
|
||||
// server-side lock
|
||||
get controlLocked() {
|
||||
return 'control' in this.$accessor.locked && this.$accessor.locked['control'] && !this.$accessor.user.admin
|
||||
}
|
||||
|
||||
get locked() {
|
||||
return this.$accessor.remote.locked
|
||||
return this.$accessor.remote.locked || (this.controlLocked && (!this.hosting || this.implicitHosting))
|
||||
}
|
||||
|
||||
get scroll() {
|
||||
@ -322,12 +340,12 @@
|
||||
|
||||
@Watch('width')
|
||||
onWidthChanged(width: number) {
|
||||
this.onResise()
|
||||
this.onResize()
|
||||
}
|
||||
|
||||
@Watch('height')
|
||||
onHeightChanged(height: number) {
|
||||
this.onResise()
|
||||
this.onResize()
|
||||
}
|
||||
|
||||
@Watch('volume')
|
||||
@ -377,24 +395,29 @@
|
||||
}
|
||||
|
||||
@Watch('clipboard')
|
||||
onClipboardChanged(clipboard: string) {
|
||||
async onClipboardChanged(clipboard: string) {
|
||||
if (this.clipboard_write_available) {
|
||||
navigator.clipboard.writeText(clipboard).catch(console.error)
|
||||
try {
|
||||
await navigator.clipboard.writeText(clipboard)
|
||||
this.$accessor.remote.setClipboard(clipboard)
|
||||
} catch (err: any) {
|
||||
this.$log.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this._container.addEventListener('resize', this.onResise)
|
||||
this._container.addEventListener('resize', this.onResize)
|
||||
this.onVolumeChanged(this.volume)
|
||||
this.onMutedChanged(this.muted)
|
||||
this.onStreamChanged(this.stream)
|
||||
this.onResise()
|
||||
this.onResize()
|
||||
|
||||
this.observer.observe(this._component)
|
||||
|
||||
this._player.addEventListener('fullscreenchange', () => {
|
||||
this.fullscreen = document.fullscreenElement !== null
|
||||
this.onResise()
|
||||
onFullscreenChange(this._player, () => {
|
||||
this.fullscreen = isFullscreen()
|
||||
this.onResize()
|
||||
})
|
||||
|
||||
this._video.addEventListener('canplaythrough', () => {
|
||||
@ -433,11 +456,9 @@
|
||||
this.$accessor.video.pause()
|
||||
})
|
||||
|
||||
document.addEventListener('focusin', this.onFocus.bind(this))
|
||||
|
||||
/* Initialize Guacamole Keyboard */
|
||||
this.keyboard.onkeydown = (key: number) => {
|
||||
if (!this.focused || !this.hosting || this.locked) {
|
||||
if (!this.hosting || this.locked) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -445,7 +466,7 @@
|
||||
return false
|
||||
}
|
||||
this.keyboard.onkeyup = (key: number) => {
|
||||
if (!this.focused || !this.hosting || this.locked) {
|
||||
if (!this.hosting || this.locked) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -457,7 +478,6 @@
|
||||
beforeDestroy() {
|
||||
this.observer.disconnect()
|
||||
this.$accessor.video.setPlayable(false)
|
||||
document.removeEventListener('focusin', this.onFocus.bind(this))
|
||||
/* Guacamole Keyboard does not provide destroy functions */
|
||||
}
|
||||
|
||||
@ -513,7 +533,7 @@
|
||||
|
||||
try {
|
||||
await this._video.play()
|
||||
this.onResise()
|
||||
this.onResize()
|
||||
} catch (err: any) {
|
||||
this.$log.error(err)
|
||||
}
|
||||
@ -551,38 +571,20 @@
|
||||
this.$accessor.remote.toggle()
|
||||
}
|
||||
|
||||
_elementRequestFullscreen(el: HTMLElement) {
|
||||
if (typeof el.requestFullscreen === 'function') {
|
||||
el.requestFullscreen()
|
||||
//@ts-ignore
|
||||
} else if (typeof el.webkitRequestFullscreen === 'function') {
|
||||
//@ts-ignore
|
||||
el.webkitRequestFullscreen()
|
||||
//@ts-ignore
|
||||
} else if (typeof el.webkitEnterFullscreen === 'function') {
|
||||
//@ts-ignore
|
||||
el.webkitEnterFullscreen()
|
||||
//@ts-ignore
|
||||
} else if (typeof el.msRequestFullScreen === 'function') {
|
||||
//@ts-ignore
|
||||
el.msRequestFullScreen()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
requestControl() {
|
||||
this.$accessor.remote.request()
|
||||
}
|
||||
|
||||
requestFullscreen() {
|
||||
// try to fullscreen player element
|
||||
if (this._elementRequestFullscreen(this._player)) {
|
||||
this.onResise()
|
||||
if (elementRequestFullscreen(this._player)) {
|
||||
this.onResize()
|
||||
return
|
||||
}
|
||||
|
||||
// fallback to fullscreen video itself (on mobile devices)
|
||||
if (this._elementRequestFullscreen(this._video)) {
|
||||
this.onResise()
|
||||
if (elementRequestFullscreen(this._video)) {
|
||||
this.onResize()
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -590,15 +592,19 @@
|
||||
requestPictureInPicture() {
|
||||
//@ts-ignore
|
||||
this._video.requestPictureInPicture()
|
||||
this.onResise()
|
||||
this.onResize()
|
||||
}
|
||||
|
||||
async onFocus() {
|
||||
if (!document.hasFocus() || !this.$accessor.active) {
|
||||
return
|
||||
openResolution(event: MouseEvent) {
|
||||
this._resolution.open(event)
|
||||
}
|
||||
|
||||
if (this.hosting && this.clipboard_read_available) {
|
||||
openClipboard() {
|
||||
this._clipboard.open()
|
||||
}
|
||||
|
||||
async syncClipboard() {
|
||||
if (this.clipboard_read_available) {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (this.clipboard !== text) {
|
||||
@ -611,9 +617,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMousePos(e: MouseEvent) {
|
||||
sendMousePos(e: MouseEvent) {
|
||||
const { w, h } = this.$accessor.video.resolution
|
||||
const rect = this._overlay.getBoundingClientRect()
|
||||
|
||||
this.$client.sendData('mousemove', {
|
||||
x: Math.round((w / rect.width) * (e.clientX - rect.left)),
|
||||
y: Math.round((h / rect.height) * (e.clientY - rect.top)),
|
||||
@ -625,7 +632,6 @@
|
||||
if (!this.hosting || this.locked) {
|
||||
return
|
||||
}
|
||||
this.onMousePos(e)
|
||||
|
||||
let x = e.deltaX
|
||||
let y = e.deltaY
|
||||
@ -648,6 +654,8 @@
|
||||
x = Math.min(Math.max(x, -this.scroll), this.scroll)
|
||||
y = Math.min(Math.max(y, -this.scroll), this.scroll)
|
||||
|
||||
this.sendMousePos(e)
|
||||
|
||||
if (!this.wheelThrottle) {
|
||||
this.wheelThrottle = true
|
||||
this.$client.sendData('wheel', { x, y })
|
||||
@ -667,7 +675,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
this.onMousePos(e)
|
||||
this.sendMousePos(e)
|
||||
this.$client.sendData('mousedown', { key: e.button + 1 })
|
||||
}
|
||||
|
||||
@ -676,7 +684,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
this.onMousePos(e)
|
||||
this.sendMousePos(e)
|
||||
this.$client.sendData('mouseup', { key: e.button + 1 })
|
||||
}
|
||||
|
||||
@ -685,7 +693,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
this.onMousePos(e)
|
||||
this.sendMousePos(e)
|
||||
}
|
||||
|
||||
onMouseEnter(e: MouseEvent) {
|
||||
@ -695,10 +703,10 @@
|
||||
numLock: e.getModifierState('NumLock'),
|
||||
scrollLock: e.getModifierState('ScrollLock'),
|
||||
})
|
||||
|
||||
this.syncClipboard()
|
||||
}
|
||||
|
||||
this._overlay.focus()
|
||||
this.onFocus()
|
||||
this.focused = true
|
||||
}
|
||||
|
||||
@ -715,28 +723,22 @@
|
||||
this.focused = false
|
||||
}
|
||||
|
||||
onResise() {
|
||||
let height = 0
|
||||
if (!this.fullscreen) {
|
||||
const { offsetWidth, offsetHeight } = this._component
|
||||
onResize() {
|
||||
const { offsetWidth, offsetHeight } = !this.fullscreen ? this._component : document.body
|
||||
this._player.style.width = `${offsetWidth}px`
|
||||
this._player.style.height = `${offsetHeight}px`
|
||||
height = offsetHeight
|
||||
} else {
|
||||
const { offsetWidth, offsetHeight } = this._player
|
||||
height = offsetHeight
|
||||
}
|
||||
|
||||
this._container.style.maxWidth = `${(this.horizontal / this.vertical) * height}px`
|
||||
this._container.style.maxWidth = `${(this.horizontal / this.vertical) * offsetHeight}px`
|
||||
this._aspect.style.paddingBottom = `${(this.vertical / this.horizontal) * 100}%`
|
||||
}
|
||||
|
||||
onResolution(event: MouseEvent) {
|
||||
this._resolution.open(event)
|
||||
@Watch('focused')
|
||||
@Watch('hosting')
|
||||
@Watch('locked')
|
||||
onFocus() {
|
||||
// in order to capture key events, overlay must be focused
|
||||
if (this.focused && this.hosting && !this.locked) {
|
||||
this._overlay.focus()
|
||||
}
|
||||
|
||||
onClipboard(event: MouseEvent) {
|
||||
this._clipboard.open(event)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -40,7 +40,7 @@ const exportMixin = {
|
||||
},
|
||||
$client() {
|
||||
return window.$client
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -53,14 +53,7 @@ const plugini18n: PluginObject<undefined> = {
|
||||
}
|
||||
|
||||
function extend(component: any) {
|
||||
return component
|
||||
.use(plugini18n)
|
||||
.use(Logger)
|
||||
.use(Axios)
|
||||
.use(Swal)
|
||||
.use(Anime)
|
||||
.use(Client)
|
||||
.extend(exportMixin)
|
||||
return component.use(plugini18n).use(Logger).use(Axios).use(Swal).use(Anime).use(Client).extend(exportMixin)
|
||||
}
|
||||
|
||||
export const NekoConnect = extend(Connect)
|
||||
|
125
client/src/locale/de-de.ts
Normal file
@ -0,0 +1,125 @@
|
||||
export const logout = 'Ausloggen'
|
||||
export const unsupported = 'Dieser Webbrowser unterstützt kein Web-RTC.'
|
||||
export const admin_loggedin = 'Du bist eingeloggt als Admin.'
|
||||
export const you = 'Du'
|
||||
export const somebody = 'Jemand'
|
||||
export const send_a_message = 'Sende eine Nachricht'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Dateien',
|
||||
settings: 'Einstellungen',
|
||||
}
|
||||
|
||||
export const connect = {
|
||||
login_title: 'Bitte Anmelden',
|
||||
invitation_title: 'Du wurdest zu diesem Raum eingeladen',
|
||||
displayname: 'Gebe deinen Benutzernamen an',
|
||||
password: 'Passwort',
|
||||
connect: 'Verbinden',
|
||||
error: 'Login Fehler',
|
||||
empty_displayname: 'Benutzername kann nicht leer sein.',
|
||||
}
|
||||
|
||||
export const context = {
|
||||
ignore: 'Ignorieren',
|
||||
unignore: 'Nicht Ignorieren',
|
||||
mute: 'Stummschalten',
|
||||
unmute: 'Nicht Stummschalten',
|
||||
release: 'Freigabesteuerung freigeben',
|
||||
take: 'Steuerung erzwingen',
|
||||
give: 'Steuerung geben',
|
||||
kick: 'Rauswerfen',
|
||||
ban: 'IP-Sperren',
|
||||
confirm: {
|
||||
kick_title: 'Kick {name}?',
|
||||
kick_text: 'Bist du sicher das du {name} rauswerfen willst?',
|
||||
ban_title: '{name} Sperren?',
|
||||
ban_text: 'Willst du {name} Sperren? Du musst den Server neustarten um es rückgängig zu machen.',
|
||||
mute_title: '{name} stummschalten?',
|
||||
mute_text: 'Bist du sicher das du {name} stummschalten willst?',
|
||||
unmute_title: '{name} stummschaltung aufheben?',
|
||||
unmute_text: 'Bist du sicher das du von {name} die stummschaltung aufheben willst?',
|
||||
button_yes: 'Ja',
|
||||
button_cancel: 'Abbrechen',
|
||||
},
|
||||
}
|
||||
|
||||
export const controls = {
|
||||
release: 'Steuerung freigeben',
|
||||
request: 'Steuerung anfordern',
|
||||
lock: 'Steuerung sperren',
|
||||
unlock: 'Steuerung entsperren',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
control: {
|
||||
lock: 'Steuerung sperren (für Nutzer)',
|
||||
unlock: 'Steuerung entsperren (für Nutzer)',
|
||||
locked: 'Steuerung gesperrt (für Nutzer)',
|
||||
unlocked: 'Steuerung entsperrt (für Nutzer)',
|
||||
notif_locked: 'Steuerung sperren für Nutzer',
|
||||
notif_unlocked: 'Steuerung entsperren für Nutzer',
|
||||
},
|
||||
login: {
|
||||
lock: 'Raum sperren (für Nutzer)',
|
||||
unlock: 'Raum entsperren (für Nutzer)',
|
||||
locked: 'Raum gesperrt (für Nutzer)',
|
||||
unlocked: 'Raum entsperrt (für Nutzer)',
|
||||
notif_locked: 'Raum gesperrt',
|
||||
notif_unlocked: 'Raum entsperrt',
|
||||
},
|
||||
file_transfer: {
|
||||
lock: 'Dateiübertragung sperren (für Nutzer)',
|
||||
unlock: 'Dateiübertragung entsperren (für Nutzer)',
|
||||
locked: 'Dateiübertragung gesperrt (für Nutzer)',
|
||||
unlocked: 'Dateiübertragung entsperrt (für Nutzer)',
|
||||
notif_locked: 'Dateiübertragung gesperrt',
|
||||
notif_unlocked: 'Dateiübertragung entsperrt',
|
||||
},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
scroll: 'Scroll-Empfindlichkeit',
|
||||
scroll_invert: 'Bildlauf umkehren',
|
||||
autoplay: 'Autoplay Video',
|
||||
ignore_emotes: 'Emotes ignorieren',
|
||||
chat_sound: 'Chat-Sound abspielen',
|
||||
keyboard_layout: 'Tastaturbelegung',
|
||||
broadcast_title: 'Live-Übertragung',
|
||||
}
|
||||
|
||||
export const connection = {
|
||||
logged_out: 'Du wurdest ausgeloggt.',
|
||||
reconnecting: 'Erneut verbinden...',
|
||||
connected: 'Verbindet',
|
||||
disconnected: 'Getrennt',
|
||||
kicked: 'Du wurdest aus diesem Raum entfernt.',
|
||||
button_confirm: 'OK',
|
||||
}
|
||||
|
||||
export const notifications = {
|
||||
connected: '{name} hat sich verbunden',
|
||||
disconnected: '{name} hat sich getrennt',
|
||||
controls_taken: '{name} hat die Steuerung genommen',
|
||||
controls_taken_force: 'nahm die Steuerung gewaltsam von',
|
||||
controls_taken_steal: 'nahm die Steuerung von {name}',
|
||||
controls_released: '{name} hat die Steuerung freigegeben',
|
||||
controls_released_force: 'hat die Steuerung gewaltsam losgelassen',
|
||||
controls_released_steal: 'hat die Steuerung freigegeben von {name}',
|
||||
controls_given: 'hat die Steuerung übergeben an {name}',
|
||||
controls_has: '{name} hat die Sterung',
|
||||
controls_has_alt: 'Aber ich habe die Person wissen lassen, dass du es wolltest',
|
||||
controls_requesting: '{name} fordert die Kontrollen an',
|
||||
resolution: 'die Auflösung geändert zu {width}x{height}@{rate}',
|
||||
banned: 'sperrte {name}',
|
||||
kicked: '{name} wurde rausgeworfen',
|
||||
muted: '{name} stummgeschaltet',
|
||||
unmuted: '{name} stummschaltung aufgehoben',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Herunterladen',
|
||||
uploads: 'Hochladen',
|
||||
upload_here: 'Klicken oder ziehen Sie Dateien zum Hochladen hierher',
|
||||
}
|
@ -7,6 +7,7 @@ export const send_a_message = 'Send a message'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Files',
|
||||
settings: 'Settings',
|
||||
}
|
||||
|
||||
@ -49,6 +50,8 @@ export const controls = {
|
||||
request: 'Request Controls',
|
||||
lock: 'Lock Controls',
|
||||
unlock: 'Unlock Controls',
|
||||
has: 'You have control',
|
||||
hasnot: 'You do not have control',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
@ -68,6 +71,14 @@ export const locks = {
|
||||
notif_locked: 'locked the room',
|
||||
notif_unlocked: 'unlocked the room',
|
||||
},
|
||||
file_transfer: {
|
||||
lock: 'Lock File Transfer (for users)',
|
||||
unlock: 'Unlock File Transfer (for users)',
|
||||
locked: 'File Transfer Locked (for users)',
|
||||
unlocked: 'File Transfer Unlocked (for users)',
|
||||
notif_locked: 'locked file transfer',
|
||||
notif_unlocked: 'unlocked file transfer',
|
||||
},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -108,3 +119,9 @@ export const notifications = {
|
||||
muted: 'muted {name}',
|
||||
unmuted: 'unmuted {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Downloads',
|
||||
uploads: 'Uploads',
|
||||
upload_here: 'Click or drag files here to upload',
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export const send_a_message = 'Enviar un mensaje'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Archivos',
|
||||
settings: 'Configuración',
|
||||
}
|
||||
|
||||
@ -51,6 +52,9 @@ export const controls = {
|
||||
request: 'Controles solicitados',
|
||||
lock: 'Controles bloqueados',
|
||||
unlock: 'Controles desbloqueados',
|
||||
// TODO
|
||||
//has: 'You have control',
|
||||
//hasnot: 'You do not have control',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
@ -71,6 +75,15 @@ export const locks = {
|
||||
notif_locked: 'bloqueó la sala',
|
||||
notif_unlocked: 'desbloqueó la sala',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -114,3 +127,9 @@ export const notifications = {
|
||||
muted: '{name} silenciado',
|
||||
unmuted: '{name} no silenciado',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Descargas',
|
||||
uploads: 'Cargar',
|
||||
upload_here: 'Haga clic o arrastre los archivos aquí para cargarlos',
|
||||
}
|
||||
|
128
client/src/locale/fi-fi.ts
Normal file
@ -0,0 +1,128 @@
|
||||
export const logout = 'kirjaudu ulos'
|
||||
export const unsupported = 'Tämä nettiselain ei tue WebRTC:tä'
|
||||
export const admin_loggedin = 'Sinä olet kirjautunut valvojana'
|
||||
export const you = 'Sinä'
|
||||
export const somebody = 'Joku'
|
||||
export const send_a_message = 'Lähetä viesti'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chatti',
|
||||
files: 'Tiedostot',
|
||||
settings: 'Asetukset',
|
||||
}
|
||||
|
||||
export const connect = {
|
||||
login_title: 'Kirjaudu Sisään',
|
||||
invitation_title: 'Sinut on kutsuttu tähän huoneeseen',
|
||||
displayname: 'Kirjoita sinun näyttönimesi',
|
||||
password: 'Salasana',
|
||||
connect: 'Liity',
|
||||
error: 'Kirjautumis virhe',
|
||||
empty_displayname: 'Näyttönimesi ei voi olla tyhjä.',
|
||||
}
|
||||
|
||||
export const context = {
|
||||
ignore: 'Estä',
|
||||
unignore: 'Poista esto',
|
||||
mute: 'Mykistä',
|
||||
unmute: 'Poista mykistys',
|
||||
release: 'Pakko vapauta kontrollit',
|
||||
take: 'Pakko ota kontrollit',
|
||||
give: 'Anna kontrollit',
|
||||
kick: 'Heitä ulos',
|
||||
ban: 'Kiellä IP',
|
||||
confirm: {
|
||||
kick_title: 'Haluatko heittää {name} ulos?',
|
||||
kick_text: 'Oletko varma että haluat heittää {name} ulos?',
|
||||
ban_title: 'Haluatko kieltää {name}?',
|
||||
ban_text: 'Haluatko kieltää {name}? Sinun pitää käynnistää palvelin uudestaan jos haluat kumota tämän.',
|
||||
mute_title: 'Haluatko mykistää {name}?',
|
||||
mute_text: 'Oletko varma että haluat mykistää {name}?',
|
||||
unmute_title: 'Poista {name} mykistys?',
|
||||
unmute_text: 'Oletko varma että haluat poistaa {name} mykistyksen?',
|
||||
button_yes: 'Kyllä',
|
||||
button_cancel: 'Peruuta',
|
||||
},
|
||||
}
|
||||
|
||||
export const controls = {
|
||||
release: 'Vapauta kontrollit',
|
||||
request: 'Pyydä kontrollit',
|
||||
lock: 'Lukitse kontrollit',
|
||||
unlock: 'Vapauta kontrollit',
|
||||
has: 'Sinulla on kontrollit',
|
||||
hasnot: 'Sinulle ei ole kontrolleja',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
control: {
|
||||
lock: 'Lukitse kontrollit (käyttäjiltä)',
|
||||
unlock: 'Vapauta kontrollit (käyttäjiltä)',
|
||||
locked: 'Kontrollit lukittu (käyttäjiltä)',
|
||||
unlocked: 'Kontrollit vapautettu (käyttäjiltä)',
|
||||
notif_locked: 'kontrollit on lukittu käyttäjiltä',
|
||||
notif_unlocked: 'kontrollit on vapautettu käyttäjille',
|
||||
},
|
||||
login: {
|
||||
lock: 'Lukitse huone (käyttäjiltä)',
|
||||
unlock: 'Vapauta huone (käyttäjiltä)',
|
||||
locked: 'Huone lukittu (käyttäjiltä)',
|
||||
unlocked: 'Huone vapautettu (käyttäjiltä)',
|
||||
notif_locked: 'lukittu huone',
|
||||
notif_unlocked: 'vapautettu huone',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
scroll: 'Scrollin herkkyys',
|
||||
scroll_invert: 'Käänteinen Scroll',
|
||||
autoplay: 'Automaattisesti toista video',
|
||||
ignore_emotes: 'Estä emojit',
|
||||
chat_sound: 'Soita viesti ääni',
|
||||
keyboard_layout: 'Näppäimistöasettelu',
|
||||
broadcast_title: 'Suora Lähetys',
|
||||
}
|
||||
|
||||
export const connection = {
|
||||
logged_out: 'Sinut on kirjattu ulos.',
|
||||
reconnecting: 'Yhteyttä yritetään palauttaa...',
|
||||
connected: 'Yhdistetty',
|
||||
disconnected: 'Katkaistu yhteys',
|
||||
kicked: 'Sinut on poistettu huoneesta.',
|
||||
button_confirm: 'OK',
|
||||
}
|
||||
|
||||
export const notifications = {
|
||||
connected: '{name} liittyi',
|
||||
disconnected: '{name} poistui',
|
||||
controls_taken: '{name} otti kontrollit',
|
||||
controls_taken_force: 'otti kontrollit pakolla',
|
||||
controls_taken_steal: 'otti kontrollit {name}',
|
||||
controls_released: '{name} vapautti kontrollit',
|
||||
controls_released_force: 'vapautti kontrollit pakolla',
|
||||
controls_released_steal: 'vapautti kontrollit {name}',
|
||||
controls_given: 'antoi kontrollit {name}',
|
||||
controls_has: '{name} on kontrollit',
|
||||
controls_has_alt: 'Kerroin henkilölle että haluat ne',
|
||||
controls_requesting: '{name} pyytää kontrolleja',
|
||||
resolution: 'vaihdettu resoluutio {width}x{height}@{rate}',
|
||||
banned: 'kielletty {name}',
|
||||
kicked: 'heitetty {name} ulos',
|
||||
muted: 'mykistetty {name}',
|
||||
unmuted: 'poistettu mykistys {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Lataukset',
|
||||
uploads: 'Lataa',
|
||||
upload_here: 'Klikkaa tai vedä tiedostoja tähän ladataksesi',
|
||||
}
|
@ -8,6 +8,7 @@ export const send_a_message = 'Envoyer un message'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Fichiers',
|
||||
settings: 'Paramètres',
|
||||
}
|
||||
|
||||
@ -46,6 +47,16 @@ export const context = {
|
||||
},
|
||||
}
|
||||
|
||||
export const controls = {
|
||||
release: 'Relacher le contrôle',
|
||||
request: 'Demander le contrôle',
|
||||
lock: 'Vérouiller le contrôle',
|
||||
unlock: 'Débloquer le contrôle',
|
||||
// TODO
|
||||
// has: 'You have control',
|
||||
// hasnot: 'You do not have control',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
// TODO
|
||||
//control: {
|
||||
@ -57,20 +68,22 @@ export const locks = {
|
||||
// notif_unlocked: 'unlocked controls for users',
|
||||
//},
|
||||
login: {
|
||||
release: 'Relacher le contrôle',
|
||||
request: 'Demander le contrôle',
|
||||
lock: 'Vérouiller le contrôle',
|
||||
unlock: 'Débloquer le contrôle',
|
||||
notif_locked: 'a vérouillé la salle',
|
||||
notif_unlocked: 'a dévérouillé la salle',
|
||||
},
|
||||
}
|
||||
|
||||
export const room = {
|
||||
lock: 'Vérouiller la salle (pour les utilisateurs)',
|
||||
unlock: 'Dévérouiller la salle (pour les utilisateurs)',
|
||||
locked: 'Salle vérouillée (pour les utilisateurs)',
|
||||
unlocked: 'Salle dévérouillée (pour les utilisateurs)',
|
||||
notif_locked: 'a vérouillé la salle',
|
||||
notif_unlocked: 'a dévérouillé la salle',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -114,3 +127,9 @@ export const notifications = {
|
||||
muted: 'a mute {name}',
|
||||
unmuted: 'a démute {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Téléchargements',
|
||||
uploads: 'Télécharger',
|
||||
upload_here: 'Cliquez ou faites glisser les fichiers ici pour les télécharger',
|
||||
}
|
||||
|
@ -4,6 +4,11 @@ import * as sk from './sk-sk'
|
||||
import * as sv from './sv-se'
|
||||
import * as nb from './nb-no'
|
||||
import * as fr from './fr-fr'
|
||||
import * as de from './de-de'
|
||||
import * as ko from './ko-kr'
|
||||
import * as fi from './fi-fi'
|
||||
import * as ru from './ru-ru'
|
||||
import * as cn from './zh-cn'
|
||||
|
||||
export const messages = {
|
||||
en,
|
||||
@ -12,4 +17,9 @@ export const messages = {
|
||||
sv,
|
||||
nb,
|
||||
fr,
|
||||
de,
|
||||
ko,
|
||||
fi,
|
||||
ru,
|
||||
cn,
|
||||
}
|
||||
|
126
client/src/locale/ko-kr.ts
Normal file
@ -0,0 +1,126 @@
|
||||
export const logout = '로그아웃'
|
||||
export const unsupported = '이 브라우저는 WebRTC 를 지원하지 않습니다'
|
||||
export const admin_loggedin = '관리자로 로그인했습니다'
|
||||
export const you = '당신'
|
||||
export const somebody = '누군가'
|
||||
export const send_a_message = '메세지 보내기'
|
||||
|
||||
export const side = {
|
||||
chat: '채팅',
|
||||
files: '파일',
|
||||
settings: '설정',
|
||||
}
|
||||
|
||||
export const connect = {
|
||||
login_title: '로그인 해주세요',
|
||||
invitation_title: '이 방에 초대됐습니다',
|
||||
displayname: '표시될 이름을 입력해주세요',
|
||||
password: '비밀번호',
|
||||
connect: '연결',
|
||||
error: '로그인 오류',
|
||||
empty_displayname: '표시될 이름은 비어있을 수 없습니다.',
|
||||
}
|
||||
|
||||
export const context = {
|
||||
ignore: '무시하기',
|
||||
unignore: '무시하기 해제',
|
||||
mute: '뮤트',
|
||||
unmute: '뮤트 해제',
|
||||
release: '강제로 조작 권한 풀기',
|
||||
take: '강제로 조작 권한 가져오기',
|
||||
give: '조작 권한 주기',
|
||||
kick: '추방',
|
||||
ban: '아이피 차단',
|
||||
confirm: {
|
||||
kick_title: '{name}님을 추방할까요?',
|
||||
kick_text: '정말로 {name}님을 추방할까요?',
|
||||
ban_title: '{name}님을 차단할까요?',
|
||||
ban_text: '정말로 {name}님을 차단할까요? 이 작업을 되돌릴려면 서버를 다시 시작해야합니다.',
|
||||
mute_title: '{name}님을 뮤트할까요?',
|
||||
mute_text: '정말로 {name}님을 뮤트할까요?',
|
||||
unmute_title: '{name}님의 뮤트를 해제할까요?',
|
||||
unmute_text: '정말로 {name}님의 뮤트를 해제할까요?',
|
||||
button_yes: '네',
|
||||
button_cancel: '아니요',
|
||||
},
|
||||
}
|
||||
|
||||
export const controls = {
|
||||
release: '조작 권한 풀기',
|
||||
request: '조작 권한 요청',
|
||||
lock: '조작 잠그기',
|
||||
unlock: '조작 잠금 해제하기',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
control: {
|
||||
lock: '조작 잠그기 (사용자)',
|
||||
unlock: '조작 잠금 해제하기 (사용자)',
|
||||
locked: '조작이 잠겼습니다 (사용자)',
|
||||
unlocked: '조작 잠금이 해제됐습니다 (사용자)',
|
||||
notif_locked: '사용자의 조작을 잠궜습니다',
|
||||
notif_unlocked: '사용자의 조작 잠금을 해제했습니다',
|
||||
},
|
||||
login: {
|
||||
lock: '방 잠그기 (사용자)',
|
||||
unlock: '방 잠금 해제하기 (사용자)',
|
||||
locked: '방이 잠겼습니다 (사용자)',
|
||||
unlocked: '방 잠금이 해제됐습니다 (사용자)',
|
||||
notif_locked: '방이 잠겼습니다',
|
||||
notif_unlocked: '방 잠금이 해제됐습니다',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
scroll: '스크롤 감도',
|
||||
scroll_invert: '스크롤 반전',
|
||||
autoplay: '동영상 자동 재생',
|
||||
ignore_emotes: '이모지 무시',
|
||||
chat_sound: '채팅 소리 재생',
|
||||
keyboard_layout: '키보드 레이아웃',
|
||||
broadcast_title: '실시간 방송',
|
||||
}
|
||||
|
||||
export const connection = {
|
||||
logged_out: '로그아웃 했습니다.',
|
||||
reconnecting: '다시 접속하는 중...',
|
||||
connected: '연결됨',
|
||||
disconnected: '연결 해제됨',
|
||||
kicked: '이 방에서 추방됐습니다.',
|
||||
button_confirm: '확인',
|
||||
}
|
||||
|
||||
export const notifications = {
|
||||
connected: '{name} 님이 접속하셨습니다',
|
||||
disconnected: '{name} 님이 퇴장하셨습니다',
|
||||
controls_taken: '{name} 님이 조작 권한을 가지셨습니다',
|
||||
controls_taken_force: '조작 권한을 강제로 가졌습니다',
|
||||
controls_taken_steal: '{name} 님으로 부터 조작 권한을 가져왔습니다',
|
||||
controls_released: '{name} 님이 조작 권한을 내려놨습니다',
|
||||
controls_released_force: '조작 권한을 강제로 내려놨습니다',
|
||||
controls_released_steal: '{name} 님의 조작 권한을 내려놨습니다',
|
||||
controls_given: '{name} 님에게 조작 권한을 줬습니다',
|
||||
controls_has: '{name} 님이 조작 권한을 가지고 있습니다',
|
||||
controls_has_alt: '가지고 있는 사람에게 가지고 싶어한다고 알려드리겠습니다',
|
||||
controls_requesting: '{name}님이 조작 권한을 요청하셨습니다',
|
||||
resolution: '해상도로 {width}x{height}@{rate} 로 변경했습니다',
|
||||
banned: '{name} 님이 차단됐습니다',
|
||||
kicked: '{name} 님이 추방됐습니다',
|
||||
muted: '{name} 님이 뮤트됐습니다',
|
||||
unmuted: '{name} 님의 뮤트가 해제됐습니다',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: '다운로드',
|
||||
uploads: '업로드',
|
||||
upload_here: '업로드할 파일을 여기로 클릭하거나 드래그하세요.',
|
||||
}
|
@ -8,6 +8,7 @@ export const send_a_message = 'Send en melding'
|
||||
|
||||
export const side = {
|
||||
chat: 'Sludring',
|
||||
files: 'Filer',
|
||||
settings: 'Innstillinger',
|
||||
}
|
||||
|
||||
@ -51,6 +52,9 @@ export const controls = {
|
||||
request: 'Forespør kontroll',
|
||||
lock: 'Lås kontrollen',
|
||||
unlock: 'Lås opp kontrollen',
|
||||
// TODO
|
||||
//has: 'You have control',
|
||||
//hasnot: 'You do not have control',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
@ -71,6 +75,15 @@ export const locks = {
|
||||
notif_locked: 'låste rommet',
|
||||
notif_unlocked: 'låste opp rommet',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -114,3 +127,9 @@ export const notifications = {
|
||||
muted: 'forstummet {name}',
|
||||
unmuted: 'opphevet forstummingen av {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Overførsler',
|
||||
uploads: 'Overfør',
|
||||
upload_here: 'Klik eller træk filer her for at uploade',
|
||||
}
|
||||
|
128
client/src/locale/ru-ru.ts
Normal file
@ -0,0 +1,128 @@
|
||||
export const logout = 'выход'
|
||||
export const unsupported = 'этот браузер не поддерживает WebRTC'
|
||||
export const admin_loggedin = 'Вы вошли как админ'
|
||||
export const you = 'Вы'
|
||||
export const somebody = 'Кто-то'
|
||||
export const send_a_message = 'Отправить сообщение'
|
||||
|
||||
export const side = {
|
||||
chat: 'Чат',
|
||||
files: 'Файлы',
|
||||
settings: 'Настройки',
|
||||
}
|
||||
|
||||
export const connect = {
|
||||
login_title: 'Пожалуйста, войдите',
|
||||
invitation_title: 'Вас пригласили в эту комнату',
|
||||
displayname: 'Введите ваше отображаемое имя',
|
||||
password: 'Пароль',
|
||||
connect: 'Подключиться',
|
||||
error: 'Ошибка входа',
|
||||
empty_displayname: 'Отображаемое имя не может быть пустым.',
|
||||
}
|
||||
|
||||
export const context = {
|
||||
ignore: 'Игнорировать',
|
||||
unignore: 'Не игнорировать',
|
||||
mute: 'Заглушить',
|
||||
unmute: 'Перестать глушить',
|
||||
release: 'Принудительно освободить управление',
|
||||
take: 'Принудительно взять управление',
|
||||
give: 'Дать управление',
|
||||
kick: 'Выкинуть',
|
||||
ban: 'Забанить IP',
|
||||
confirm: {
|
||||
kick_title: 'Выкинуть {name}?',
|
||||
kick_text: 'Вы уверены, что хотите выкинуть {name}?',
|
||||
ban_title: 'Забанить {name}?',
|
||||
ban_text: 'Вы хотите забанить {name}? Для отмены придётся перезапустить сервер.',
|
||||
mute_title: 'Заглушить {name}?',
|
||||
mute_text: 'Вы уверены, что хотите заглушить {name}?',
|
||||
unmute_title: 'Перестать глушить {name}?',
|
||||
unmute_text: 'Вы хотите перестать глушить {name}?',
|
||||
button_yes: 'Да',
|
||||
button_cancel: 'Отмена',
|
||||
},
|
||||
}
|
||||
|
||||
export const controls = {
|
||||
release: 'Освободить управление',
|
||||
request: 'Запросить управление',
|
||||
lock: 'Закрепить управление',
|
||||
unlock: 'Открепить управление',
|
||||
has: 'Управление у вас',
|
||||
hasnot: 'Вы не управляете',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
control: {
|
||||
lock: 'Закрепить управление (для пользователей)',
|
||||
unlock: 'Открепить управление (для пользователей)',
|
||||
locked: 'Управление закреплено (для пользователей)',
|
||||
unlocked: 'Управление откреплено (для пользователей)',
|
||||
notif_locked: 'закреплено управление для пользователей',
|
||||
notif_unlocked: 'откреплено управление для пользователей',
|
||||
},
|
||||
login: {
|
||||
lock: 'Закрыть комнату (для пользователей)',
|
||||
unlock: 'Открыть комнату (для пользователей)',
|
||||
locked: 'Комната закрыта (для пользователей)',
|
||||
unlocked: 'Комната открыта (для пользователей)',
|
||||
notif_locked: 'комната закрыта',
|
||||
notif_unlocked: 'комната открыта',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
scroll: 'Чувствительность прокрутки',
|
||||
scroll_invert: 'Инвертировать прокрутку',
|
||||
autoplay: 'Автовоспроизведение видео',
|
||||
ignore_emotes: 'Игнорировать эмоции',
|
||||
chat_sound: 'Проигрывать звук чата',
|
||||
keyboard_layout: 'Раскладка клавиатуры',
|
||||
broadcast_title: 'Прямой эфир',
|
||||
}
|
||||
|
||||
export const connection = {
|
||||
logged_out: 'Вы вышли.',
|
||||
reconnecting: 'Переподключение...',
|
||||
connected: 'Подключено',
|
||||
disconnected: 'Отключено',
|
||||
kicked: 'Вас выкинули из комнаты.',
|
||||
button_confirm: 'ОК',
|
||||
}
|
||||
|
||||
export const notifications = {
|
||||
connected: '{name} подключился',
|
||||
disconnected: '{name} отключился',
|
||||
controls_taken: '{name} взял управление',
|
||||
controls_taken_force: 'взял управление принудительно',
|
||||
controls_taken_steal: 'взял управление у {name}',
|
||||
controls_released: '{name} освободил управление',
|
||||
controls_released_force: 'освободил управление принудительно',
|
||||
controls_released_steal: 'освободил управление у {name}',
|
||||
controls_given: 'дал управление {name}',
|
||||
controls_has: '{name} теперь управляет',
|
||||
controls_has_alt: 'Мы уведомим пользователя о запросе',
|
||||
controls_requesting: '{name} запрашивает управление',
|
||||
resolution: 'разрешение изменено на {width}x{height}@{rate}',
|
||||
banned: 'забанен {name}',
|
||||
kicked: 'выкинут {name}',
|
||||
muted: 'заглушен {name}',
|
||||
unmuted: 'не заглушен {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Загрузки',
|
||||
uploads: 'Загрузить',
|
||||
upload_here: 'Нажмите или перетащите сюда файлы для загрузки',
|
||||
}
|
@ -8,6 +8,7 @@ export const send_a_message = 'Odoslať správu'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Súbory',
|
||||
settings: 'Nastavenia',
|
||||
}
|
||||
|
||||
@ -51,6 +52,9 @@ export const controls = {
|
||||
request: 'Požiadať o ovládanie',
|
||||
lock: 'Zamknúť ovládanie',
|
||||
unlock: 'Odomknúť ovládanie',
|
||||
// TODO
|
||||
//has: 'You have control',
|
||||
//hasnot: 'You do not have control',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
@ -70,6 +74,15 @@ export const locks = {
|
||||
notif_locked: 'miestnosť bola zamknutá',
|
||||
notif_unlocked: 'miestnosť bola odomknutá',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -110,3 +123,9 @@ export const notifications = {
|
||||
muted: 'zakázal chat používateľovi {name}',
|
||||
unmuted: 'povolil chat používateľovi {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Stiahnutia',
|
||||
uploads: 'Nahrávanie',
|
||||
upload_here: 'Kliknutím alebo pretiahnutím súborov sem ich môžete nahrať',
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export const send_a_message = 'Skicka ett meddelande'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chatt',
|
||||
files: 'Filer',
|
||||
settings: 'Inställningar',
|
||||
}
|
||||
|
||||
@ -51,6 +52,9 @@ export const controls = {
|
||||
request: 'Fråga om kontroll',
|
||||
lock: 'Lås kontrollen',
|
||||
unlock: 'Lås upp kontrollen',
|
||||
// TODO
|
||||
//has: 'You have control',
|
||||
//hasnot: 'You do not have control',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
@ -71,6 +75,15 @@ export const locks = {
|
||||
notif_locked: 'låste rummet',
|
||||
notif_unlocked: 'låste upp rummet',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -114,3 +127,9 @@ export const notifications = {
|
||||
muted: 'tystade {name}',
|
||||
unmuted: 'tog bort tystningen på {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Nedladdningar',
|
||||
uploads: 'Ladda upp',
|
||||
upload_here: 'Klicka eller dra filer hit för att ladda upp dem',
|
||||
}
|
||||
|
128
client/src/locale/zh-cn.ts
Normal file
@ -0,0 +1,128 @@
|
||||
export const logout = '登出'
|
||||
export const unsupported = '你的浏览器不支持 WebRTC'
|
||||
export const admin_loggedin = '您以管理钻身份登陆'
|
||||
export const you = '你'
|
||||
export const somebody = '某人'
|
||||
export const send_a_message = '发送消息'
|
||||
|
||||
export const side = {
|
||||
chat: '聊天',
|
||||
files: '文件',
|
||||
settings: '设置',
|
||||
}
|
||||
|
||||
export const connect = {
|
||||
login_title: '登录',
|
||||
invitation_title: '你已被邀请到这个房间',
|
||||
displayname: '您的姓名',
|
||||
password: '密码',
|
||||
connect: '连接',
|
||||
error: '登录错误',
|
||||
empty_displayname: '显示名称不能为空',
|
||||
}
|
||||
|
||||
export const context = {
|
||||
ignore: '忽略',
|
||||
unignore: '取消忽略',
|
||||
mute: '静音',
|
||||
unmute: '取消静音',
|
||||
release: '强制释放控制',
|
||||
take: '牵制控制',
|
||||
give: '给予控制',
|
||||
kick: '踢出',
|
||||
ban: '禁止 IP',
|
||||
confirm: {
|
||||
kick_title: '踢出 {name}?',
|
||||
kick_text: '你确定你要替 {name}?',
|
||||
ban_title: '禁止 {name}?',
|
||||
ban_text: '你是否想禁止 {name}? 你将需要重新启动服务器来撤销这一做法.',
|
||||
mute_title: '静音 {name}?',
|
||||
mute_text: '你确定你要精印吗 {name}?',
|
||||
unmute_title: '取消静音 {name}?',
|
||||
unmute_text: '你想去下静音吗 {name}?',
|
||||
button_yes: '是',
|
||||
button_cancel: '取消',
|
||||
},
|
||||
}
|
||||
|
||||
export const controls = {
|
||||
release: '释放控制',
|
||||
request: '请求控制',
|
||||
lock: '锁定控制',
|
||||
unlock: '解锁控制',
|
||||
has: '你有控制',
|
||||
hasnot: '你没有控制',
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
control: {
|
||||
lock: '对所有用户进行锁定控制',
|
||||
unlock: '对所有用户进行解锁控制',
|
||||
locked: '锁定的控制装置',
|
||||
unlocked: '解锁的控制装置',
|
||||
notif_locked: '为用户锁定控制',
|
||||
notif_unlocked: '为用户解锁控制',
|
||||
},
|
||||
login: {
|
||||
lock: '所有用户的锁定室',
|
||||
unlock: '所有用户的解锁室',
|
||||
locked: '为所有用户锁定的房间',
|
||||
unlocked: '为所有用户解锁的房间',
|
||||
notif_locked: '锁上房间',
|
||||
notif_unlocked: '解锁房间',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
scroll: '滚动敏感度',
|
||||
scroll_invert: '反转滚动敏感度',
|
||||
autoplay: '自动播放视频',
|
||||
ignore_emotes: '忽略表情符号',
|
||||
chat_sound: '播放聊天声音',
|
||||
keyboard_layout: '键盘布局',
|
||||
broadcast_title: '现场流媒体',
|
||||
}
|
||||
|
||||
export const connection = {
|
||||
logged_out: '你已登出',
|
||||
reconnecting: '正在重新连接',
|
||||
connected: '已连接',
|
||||
disconnected: '已断开',
|
||||
kicked: '你已被踢出',
|
||||
button_confirm: '好的',
|
||||
}
|
||||
|
||||
export const notifications = {
|
||||
connected: '{name} 已连接',
|
||||
disconnected: '{name} 已断开',
|
||||
controls_taken: '{name} 采取了控制',
|
||||
controls_taken_force: '被迫接受控制',
|
||||
controls_taken_steal: '掌握控制权 从 {name}',
|
||||
controls_released: '{name} 释放控制',
|
||||
controls_released_force: '强制解除控制',
|
||||
controls_released_steal: '强制解除控制 从 {name}',
|
||||
controls_given: '将控制权交给 {name}',
|
||||
controls_has: '{name} 拥有控制权',
|
||||
controls_has_alt: '但我让那个人知道你想要它',
|
||||
controls_requesting: '{name} 正在请求控制',
|
||||
resolution: '将分辨率改为 {width}x{height}@{rate}',
|
||||
banned: '被禁止 {name}',
|
||||
kicked: '被踢的 {name}',
|
||||
muted: '鸟粪 {name}',
|
||||
unmuted: '取消静音 {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: '下载',
|
||||
uploads: '上传',
|
||||
upload_here: '点击或拖动文件到这里来上传',
|
||||
}
|
@ -2,7 +2,14 @@ import EventEmitter from 'eventemitter3'
|
||||
import { OPCODE } from './data'
|
||||
import { EVENT, WebSocketEvents } from './events'
|
||||
|
||||
import { WebSocketMessages, WebSocketPayloads, SignalProvidePayload, SignalCandidatePayload } from './messages'
|
||||
import {
|
||||
WebSocketMessages,
|
||||
WebSocketPayloads,
|
||||
SignalProvidePayload,
|
||||
SignalCandidatePayload,
|
||||
SignalOfferPayload,
|
||||
SignalAnswerMessage,
|
||||
} from './messages'
|
||||
|
||||
export interface BaseEvents {
|
||||
info: (...message: any[]) => void
|
||||
@ -180,7 +187,7 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
this._ws!.send(JSON.stringify({ event, ...payload }))
|
||||
}
|
||||
|
||||
public async createPeer(sdp: string, lite: boolean, servers: RTCIceServer[]) {
|
||||
public async createPeer(lite: boolean, servers: RTCIceServer[]) {
|
||||
this.emit('debug', `creating peer`)
|
||||
if (!this.socketOpen) {
|
||||
this.emit(
|
||||
@ -243,13 +250,32 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
}
|
||||
|
||||
this._peer.ontrack = this.onTrack.bind(this)
|
||||
this._peer.addTransceiver('audio', { direction: 'recvonly' })
|
||||
this._peer.addTransceiver('video', { direction: 'recvonly' })
|
||||
|
||||
this._peer.onnegotiationneeded = async () => {
|
||||
this.emit('warn', `negotiation is needed`)
|
||||
|
||||
const d = await this._peer!.createOffer()
|
||||
this._peer!.setLocalDescription(d)
|
||||
|
||||
this._ws!.send(
|
||||
JSON.stringify({
|
||||
event: EVENT.SIGNAL.OFFER,
|
||||
sdp: d.sdp,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
this._channel = this._peer.createDataChannel('data')
|
||||
this._channel.onerror = this.onError.bind(this)
|
||||
this._channel.onmessage = this.onData.bind(this)
|
||||
this._channel.onclose = this.onDisconnected.bind(this, new Error('peer data channel closed'))
|
||||
}
|
||||
|
||||
public async setRemoteOffer(sdp: string) {
|
||||
if (!this._peer) {
|
||||
this.emit('warn', `attempting to set remote offer while disconnected`)
|
||||
return
|
||||
}
|
||||
|
||||
this._peer.setRemoteDescription({ type: 'offer', sdp })
|
||||
|
||||
@ -274,7 +300,16 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
}
|
||||
}
|
||||
|
||||
private onMessage(e: MessageEvent) {
|
||||
public async setRemoteAnswer(sdp: string) {
|
||||
if (!this._peer) {
|
||||
this.emit('warn', `attempting to set remote answer while disconnected`)
|
||||
return
|
||||
}
|
||||
|
||||
this._peer.setRemoteDescription({ type: 'answer', sdp })
|
||||
}
|
||||
|
||||
private async onMessage(e: MessageEvent) {
|
||||
const { event, ...payload } = JSON.parse(e.data) as WebSocketMessages
|
||||
|
||||
this.emit('debug', `received websocket event ${event} ${payload ? `with payload: ` : ''}`, payload)
|
||||
@ -282,7 +317,20 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
if (event === EVENT.SIGNAL.PROVIDE) {
|
||||
const { sdp, lite, ice, id } = payload as SignalProvidePayload
|
||||
this._id = id
|
||||
this.createPeer(sdp, lite, ice)
|
||||
await this.createPeer(lite, ice)
|
||||
await this.setRemoteOffer(sdp)
|
||||
return
|
||||
}
|
||||
|
||||
if (event === EVENT.SIGNAL.OFFER) {
|
||||
const { sdp } = payload as SignalOfferPayload
|
||||
await this.setRemoteOffer(sdp)
|
||||
return
|
||||
}
|
||||
|
||||
if (event === EVENT.SIGNAL.ANSWER) {
|
||||
const { sdp } = payload as SignalAnswerMessage
|
||||
await this.setRemoteAnswer(sdp)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -10,10 +10,12 @@ export const EVENT = {
|
||||
|
||||
// Websocket Events
|
||||
SYSTEM: {
|
||||
INIT: 'system/init',
|
||||
DISCONNECT: 'system/disconnect',
|
||||
ERROR: 'system/error',
|
||||
},
|
||||
SIGNAL: {
|
||||
OFFER: 'signal/offer',
|
||||
ANSWER: 'signal/answer',
|
||||
PROVIDE: 'signal/provide',
|
||||
CANDIDATE: 'signal/candidate',
|
||||
@ -36,6 +38,10 @@ export const EVENT = {
|
||||
MESSAGE: 'chat/message',
|
||||
EMOTE: 'chat/emote',
|
||||
},
|
||||
FILETRANSFER: {
|
||||
LIST: 'filetransfer/list',
|
||||
REFRESH: 'filetransfer/refresh',
|
||||
},
|
||||
SCREEN: {
|
||||
CONFIGURATIONS: 'screen/configurations',
|
||||
RESOLUTION: 'screen/resolution',
|
||||
@ -67,6 +73,7 @@ export type WebSocketEvents =
|
||||
| MemberEvents
|
||||
| SignalEvents
|
||||
| ChatEvents
|
||||
| FileTransferEvents
|
||||
| ScreenEvents
|
||||
| BroadcastEvents
|
||||
| AdminEvents
|
||||
@ -81,8 +88,17 @@ export type ControlEvents =
|
||||
|
||||
export type SystemEvents = typeof EVENT.SYSTEM.DISCONNECT
|
||||
export type MemberEvents = typeof EVENT.MEMBER.LIST | typeof EVENT.MEMBER.CONNECTED | typeof EVENT.MEMBER.DISCONNECTED
|
||||
export type SignalEvents = typeof EVENT.SIGNAL.ANSWER | typeof EVENT.SIGNAL.PROVIDE | typeof EVENT.SIGNAL.CANDIDATE
|
||||
|
||||
export type SignalEvents =
|
||||
| typeof EVENT.SIGNAL.OFFER
|
||||
| typeof EVENT.SIGNAL.ANSWER
|
||||
| typeof EVENT.SIGNAL.PROVIDE
|
||||
| typeof EVENT.SIGNAL.CANDIDATE
|
||||
|
||||
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
|
||||
|
||||
export type FileTransferEvents = typeof EVENT.FILETRANSFER.LIST | typeof EVENT.FILETRANSFER.REFRESH
|
||||
|
||||
export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET
|
||||
|
||||
export type BroadcastEvents =
|
||||
|
@ -22,6 +22,9 @@ import {
|
||||
AdminPayload,
|
||||
AdminTargetPayload,
|
||||
AdminLockMessage,
|
||||
SystemInitPayload,
|
||||
AdminLockResource,
|
||||
FileTransferListPayload,
|
||||
} from './messages'
|
||||
|
||||
interface NekoEvents extends BaseEvents {}
|
||||
@ -44,6 +47,8 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
||||
this.$vue = vue
|
||||
this.$accessor = vue.$accessor
|
||||
this.url = url
|
||||
// convert ws url to http url
|
||||
this.$vue.$http.defaults.baseURL = url.replace(/^ws/, 'http').replace(/\/ws$/, '')
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
@ -131,6 +136,19 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
||||
/////////////////////////////
|
||||
// System Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks, file_transfer }: SystemInitPayload) {
|
||||
this.$accessor.remote.setImplicitHosting(implicit_hosting)
|
||||
this.$accessor.remote.setFileTransfer(file_transfer)
|
||||
|
||||
for (const resource in locks) {
|
||||
this[EVENT.ADMIN.LOCK]({
|
||||
event: EVENT.ADMIN.LOCK,
|
||||
resource: resource as AdminLockResource,
|
||||
id: locks[resource],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
protected [EVENT.SYSTEM.DISCONNECT]({ message }: SystemMessagePayload) {
|
||||
if (message == 'kicked') {
|
||||
this.$accessor.logout()
|
||||
@ -337,6 +355,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
||||
this.$accessor.chat.newEmote({ type: emote })
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// File Transfer Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.FILETRANSFER.LIST]({ cwd, files }: FileTransferListPayload) {
|
||||
this.$accessor.files.setCwd(cwd)
|
||||
this.$accessor.files.setFileList(files)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Screen Events
|
||||
/////////////////////////////
|
||||
|
@ -8,12 +8,14 @@ import {
|
||||
ChatEvents,
|
||||
ScreenEvents,
|
||||
AdminEvents,
|
||||
FileTransferEvents,
|
||||
} from './events'
|
||||
import { Member, ScreenConfigurations, ScreenResolution } from './types'
|
||||
import { FileListItem, Member, ScreenConfigurations, ScreenResolution } from './types'
|
||||
|
||||
export type WebSocketMessages =
|
||||
| WebSocketMessage
|
||||
| SignalProvideMessage
|
||||
| SignalOfferMessage
|
||||
| SignalAnswerMessage
|
||||
| SignalCandidateMessage
|
||||
| MemberListMessage
|
||||
@ -26,6 +28,7 @@ export type WebSocketMessages =
|
||||
|
||||
export type WebSocketPayloads =
|
||||
| SignalProvidePayload
|
||||
| SignalOfferPayload
|
||||
| SignalAnswerPayload
|
||||
| SignalCandidatePayload
|
||||
| MemberListPayload
|
||||
@ -50,6 +53,16 @@ export interface WebSocketMessage {
|
||||
/*
|
||||
SYSTEM MESSAGES/PAYLOADS
|
||||
*/
|
||||
// system/init
|
||||
export interface SystemInit extends WebSocketMessage, SystemInitPayload {
|
||||
event: typeof EVENT.SYSTEM.INIT
|
||||
}
|
||||
export interface SystemInitPayload {
|
||||
implicit_hosting: boolean
|
||||
locks: Record<string, string>
|
||||
file_transfer: boolean
|
||||
}
|
||||
|
||||
// system/disconnect
|
||||
// system/error
|
||||
export interface SystemMessage extends WebSocketMessage, SystemMessagePayload {
|
||||
@ -74,6 +87,14 @@ export interface SignalProvidePayload {
|
||||
sdp: string
|
||||
}
|
||||
|
||||
// signal/offer
|
||||
export interface SignalOfferMessage extends WebSocketMessage, SignalOfferPayload {
|
||||
event: typeof EVENT.SIGNAL.OFFER
|
||||
}
|
||||
export interface SignalOfferPayload {
|
||||
sdp: string
|
||||
}
|
||||
|
||||
// signal/answer
|
||||
export interface SignalAnswerMessage extends WebSocketMessage, SignalAnswerPayload {
|
||||
event: typeof EVENT.SIGNAL.ANSWER
|
||||
@ -173,6 +194,18 @@ export interface EmojiSendPayload {
|
||||
emote: string
|
||||
}
|
||||
|
||||
/*
|
||||
FILE TRANSFER PAYLOADS
|
||||
*/
|
||||
export interface FileTransferListMessage extends WebSocketMessage, FileTransferListPayload {
|
||||
event: FileTransferEvents
|
||||
}
|
||||
|
||||
export interface FileTransferListPayload {
|
||||
cwd: string
|
||||
files: FileListItem[]
|
||||
}
|
||||
|
||||
/*
|
||||
SCREEN PAYLOADS
|
||||
*/
|
||||
@ -229,7 +262,7 @@ export interface AdminLockMessage extends WebSocketMessage, AdminLockPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type AdminLockResource = 'login' | 'control'
|
||||
export type AdminLockResource = 'login' | 'control' | 'file_transfer'
|
||||
|
||||
export interface AdminLockPayload {
|
||||
resource: AdminLockResource
|
||||
|
@ -22,3 +22,20 @@ export interface ScreenResolution {
|
||||
height: number
|
||||
rate: number
|
||||
}
|
||||
|
||||
export interface FileListItem {
|
||||
name: string
|
||||
type: 'file' | 'dir'
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface FileTransfer {
|
||||
id: number
|
||||
name: string
|
||||
direction: 'upload' | 'download'
|
||||
size: number
|
||||
progress: number
|
||||
status: 'pending' | 'inprogress' | 'completed' | 'failed'
|
||||
error?: string
|
||||
abortController?: AbortController
|
||||
}
|
||||
|
72
client/src/store/files.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { actionTree, getterTree, mutationTree } from 'typed-vuex'
|
||||
import { FileListItem, FileTransfer } from '~/neko/types'
|
||||
import { EVENT } from '~/neko/events'
|
||||
import { accessor } from '~/store'
|
||||
|
||||
export const state = () => ({
|
||||
cwd: '',
|
||||
files: [] as FileListItem[],
|
||||
transfers: [] as FileTransfer[],
|
||||
})
|
||||
|
||||
export const getters = getterTree(state, {
|
||||
//
|
||||
})
|
||||
|
||||
export const mutations = mutationTree(state, {
|
||||
_setCwd(state, cwd: string) {
|
||||
state.cwd = cwd
|
||||
},
|
||||
|
||||
_setFileList(state, files: FileListItem[]) {
|
||||
state.files = files
|
||||
},
|
||||
|
||||
_addTransfer(state, transfer: FileTransfer) {
|
||||
state.transfers = [...state.transfers, transfer]
|
||||
},
|
||||
|
||||
_removeTransfer(state, transfer: FileTransfer) {
|
||||
state.transfers = state.transfers.filter((t) => t.id !== transfer.id)
|
||||
},
|
||||
})
|
||||
|
||||
export const actions = actionTree(
|
||||
{ state, getters, mutations },
|
||||
{
|
||||
setCwd(store, cwd: string) {
|
||||
accessor.files._setCwd(cwd)
|
||||
},
|
||||
|
||||
setFileList(store, files: FileListItem[]) {
|
||||
accessor.files._setFileList(files)
|
||||
},
|
||||
|
||||
addTransfer(store, transfer: FileTransfer) {
|
||||
if (transfer.status !== 'pending') {
|
||||
return
|
||||
}
|
||||
accessor.files._addTransfer(transfer)
|
||||
},
|
||||
|
||||
removeTransfer(store, transfer: FileTransfer) {
|
||||
accessor.files._removeTransfer(transfer)
|
||||
},
|
||||
|
||||
cancelAllTransfers(store) {
|
||||
for (const t of accessor.files.transfers) {
|
||||
if (t.status !== 'completed') {
|
||||
t.abortController?.abort()
|
||||
}
|
||||
accessor.files.removeTransfer(t)
|
||||
}
|
||||
},
|
||||
|
||||
refresh(store) {
|
||||
if (!accessor.connected) {
|
||||
return
|
||||
}
|
||||
$client.sendMessage(EVENT.FILETRANSFER.REFRESH)
|
||||
},
|
||||
},
|
||||
)
|
@ -1,12 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { useAccessor, mutationTree, actionTree } from 'typed-vuex'
|
||||
import { useAccessor, mutationTree, getterTree, actionTree } from 'typed-vuex'
|
||||
import { EVENT } from '~/neko/events'
|
||||
import { AdminLockResource } from '~/neko/messages'
|
||||
import { get, set } from '~/utils/localstorage'
|
||||
|
||||
import * as video from './video'
|
||||
import * as chat from './chat'
|
||||
import * as files from './files'
|
||||
import * as remote from './remote'
|
||||
import * as user from './user'
|
||||
import * as settings from './settings'
|
||||
@ -55,8 +56,12 @@ export const mutations = mutationTree(state, {
|
||||
},
|
||||
})
|
||||
|
||||
export const getters = getterTree(state, {
|
||||
isLocked: (state) => (resource: AdminLockResource) => resource in state.locked && state.locked[resource],
|
||||
})
|
||||
|
||||
export const actions = actionTree(
|
||||
{ state, mutations },
|
||||
{ state, getters, mutations },
|
||||
{
|
||||
initialise(store) {
|
||||
accessor.emoji.initialise()
|
||||
@ -79,6 +84,14 @@ export const actions = actionTree(
|
||||
$client.sendMessage(EVENT.ADMIN.UNLOCK, { resource })
|
||||
},
|
||||
|
||||
toggleLock(_, resource: AdminLockResource) {
|
||||
if (accessor.isLocked(resource)) {
|
||||
accessor.unlock(resource)
|
||||
} else {
|
||||
accessor.lock(resource)
|
||||
}
|
||||
},
|
||||
|
||||
login({ state }, { displayname, password }: { displayname: string; password: string }) {
|
||||
accessor.setLogin({ displayname, password })
|
||||
$client.login(password, displayname)
|
||||
@ -97,7 +110,8 @@ export const storePattern = {
|
||||
state,
|
||||
mutations,
|
||||
actions,
|
||||
modules: { video, chat, user, remote, settings, client, emoji },
|
||||
getters,
|
||||
modules: { video, chat, files, user, remote, settings, client, emoji },
|
||||
}
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
@ -12,19 +12,20 @@ export const state = () => ({
|
||||
id: '',
|
||||
clipboard: '',
|
||||
locked: false,
|
||||
|
||||
implicitHosting: true,
|
||||
fileTransfer: true,
|
||||
keyboardModifierState: -1,
|
||||
})
|
||||
|
||||
export const getters = getterTree(state, {
|
||||
hosting: (state, getters, root) => {
|
||||
return root.user.id === state.id
|
||||
return root.user.id === state.id || state.implicitHosting
|
||||
},
|
||||
hosted: (state, getters, root) => {
|
||||
return state.id !== ''
|
||||
return state.id !== '' || state.implicitHosting
|
||||
},
|
||||
host: (state, getters, root) => {
|
||||
return root.user.members[state.id] || null
|
||||
return root.user.members[state.id] || (state.implicitHosting && root.user.id) || null
|
||||
},
|
||||
})
|
||||
|
||||
@ -49,11 +50,18 @@ export const mutations = mutationTree(state, {
|
||||
state.locked = locked
|
||||
},
|
||||
|
||||
setImplicitHosting(state, val: boolean) {
|
||||
state.implicitHosting = val
|
||||
},
|
||||
|
||||
setFileTransfer(state, val: boolean) {
|
||||
state.fileTransfer = val
|
||||
},
|
||||
|
||||
reset(state) {
|
||||
state.id = ''
|
||||
state.clipboard = ''
|
||||
state.locked = false
|
||||
state.keyboardModifierState = -1
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
@ -35,7 +36,9 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
|
||||
/**
|
||||
* Reference to this Guacamole.Keyboard.
|
||||
*
|
||||
* @private
|
||||
* @type {!Guacamole.Keyboard}
|
||||
*/
|
||||
var guac_keyboard = this;
|
||||
|
||||
@ -44,7 +47,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* instance with respect to other Guacamole.Keyboard instances.
|
||||
*
|
||||
* @private
|
||||
* @type {Number}
|
||||
* @type {!number}
|
||||
*/
|
||||
var guacKeyboardID = Guacamole.Keyboard._nextID++;
|
||||
|
||||
@ -54,7 +57,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
*
|
||||
* @private
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @type {!string}
|
||||
*/
|
||||
var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID;
|
||||
|
||||
@ -63,9 +66,12 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* with this Guacamole.Keyboard in focus.
|
||||
*
|
||||
* @event
|
||||
* @param {Number} keysym The keysym of the key being pressed.
|
||||
* @return {Boolean} true if the key event should be allowed through to the
|
||||
* browser, false otherwise.
|
||||
* @param {!number} keysym
|
||||
* The keysym of the key being pressed.
|
||||
*
|
||||
* @return {!boolean}
|
||||
* true if the key event should be allowed through to the browser,
|
||||
* false otherwise.
|
||||
*/
|
||||
this.onkeydown = null;
|
||||
|
||||
@ -74,7 +80,8 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* with this Guacamole.Keyboard in focus.
|
||||
*
|
||||
* @event
|
||||
* @param {Number} keysym The keysym of the key being released.
|
||||
* @param {!number} keysym
|
||||
* The keysym of the key being released.
|
||||
*/
|
||||
this.onkeyup = null;
|
||||
|
||||
@ -84,14 +91,14 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* reliably detect that quirk is to platform/browser-sniff.
|
||||
*
|
||||
* @private
|
||||
* @type {Object.<String, Boolean>}
|
||||
* @type {!Object.<string, boolean>}
|
||||
*/
|
||||
var quirks = {
|
||||
|
||||
/**
|
||||
* Whether keyup events are universally unreliable.
|
||||
*
|
||||
* @type {Boolean}
|
||||
* @type {!boolean}
|
||||
*/
|
||||
keyupUnreliable: false,
|
||||
|
||||
@ -99,7 +106,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* Whether the Alt key is actually a modifier for typable keys and is
|
||||
* thus never used for keyboard shortcuts.
|
||||
*
|
||||
* @type {Boolean}
|
||||
* @type {!boolean}
|
||||
*/
|
||||
altIsTypableOnly: false,
|
||||
|
||||
@ -107,7 +114,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* Whether we can rely on receiving a keyup event for the Caps Lock
|
||||
* key.
|
||||
*
|
||||
* @type {Boolean}
|
||||
* @type {!boolean}
|
||||
*/
|
||||
capsLockKeyupUnreliable: false
|
||||
|
||||
@ -137,26 +144,75 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
*
|
||||
* @private
|
||||
* @constructor
|
||||
* @param {KeyboardEvent} [orig]
|
||||
* The relevant DOM keyboard event.
|
||||
*/
|
||||
var KeyEvent = function() {
|
||||
var KeyEvent = function KeyEvent(orig) {
|
||||
|
||||
/**
|
||||
* Reference to this key event.
|
||||
*
|
||||
* @private
|
||||
* @type {!KeyEvent}
|
||||
*/
|
||||
var key_event = this;
|
||||
|
||||
/**
|
||||
* The JavaScript key code of the key pressed. For most events (keydown
|
||||
* and keyup), this is a scancode-like value related to the position of
|
||||
* the key on the US English "Qwerty" keyboard. For keypress events,
|
||||
* this is the Unicode codepoint of the character that would be typed
|
||||
* by the key pressed.
|
||||
*
|
||||
* @type {!number}
|
||||
*/
|
||||
this.keyCode = orig ? (orig.which || orig.keyCode) : 0;
|
||||
|
||||
/**
|
||||
* The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
|
||||
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
|
||||
*
|
||||
* @type {!string}
|
||||
*/
|
||||
this.keyIdentifier = orig && orig.keyIdentifier;
|
||||
|
||||
/**
|
||||
* The standard name of the key pressed, as defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
*
|
||||
* @type {!string}
|
||||
*/
|
||||
this.key = orig && orig.key;
|
||||
|
||||
/**
|
||||
* The location on the keyboard corresponding to the key pressed, as
|
||||
* defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
*
|
||||
* @type {!number}
|
||||
*/
|
||||
this.location = orig ? getEventLocation(orig) : 0;
|
||||
|
||||
/**
|
||||
* The state of all local keyboard modifiers at the time this event was
|
||||
* received.
|
||||
*
|
||||
* @type {!Guacamole.Keyboard.ModifierState}
|
||||
*/
|
||||
this.modifiers = orig ? Guacamole.Keyboard.ModifierState.fromKeyboardEvent(orig) : new Guacamole.Keyboard.ModifierState();
|
||||
|
||||
/**
|
||||
* An arbitrary timestamp in milliseconds, indicating this event's
|
||||
* position in time relative to other events.
|
||||
*
|
||||
* @type {Number}
|
||||
* @type {!number}
|
||||
*/
|
||||
this.timestamp = new Date().getTime();
|
||||
|
||||
/**
|
||||
* Whether the default action of this key event should be prevented.
|
||||
*
|
||||
* @type {Boolean}
|
||||
* @type {!boolean}
|
||||
*/
|
||||
this.defaultPrevented = false;
|
||||
|
||||
@ -165,7 +221,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* by a best-effort guess using available event properties and keyboard
|
||||
* state.
|
||||
*
|
||||
* @type {Number}
|
||||
* @type {number}
|
||||
*/
|
||||
this.keysym = null;
|
||||
|
||||
@ -174,7 +230,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* If false, the keysym may still be valid, but it's only a best guess,
|
||||
* and future key events may be a better source of information.
|
||||
*
|
||||
* @type {Boolean}
|
||||
* @type {!boolean}
|
||||
*/
|
||||
this.reliable = false;
|
||||
|
||||
@ -182,8 +238,9 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* Returns the number of milliseconds elapsed since this event was
|
||||
* received.
|
||||
*
|
||||
* @return {Number} The number of milliseconds elapsed since this
|
||||
* event was received.
|
||||
* @return {!number}
|
||||
* The number of milliseconds elapsed since this event was
|
||||
* received.
|
||||
*/
|
||||
this.getAge = function() {
|
||||
return new Date().getTime() - key_event.timestamp;
|
||||
@ -199,62 +256,23 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* @private
|
||||
* @constructor
|
||||
* @augments Guacamole.Keyboard.KeyEvent
|
||||
* @param {Number} keyCode The JavaScript key code of the key pressed.
|
||||
* @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key
|
||||
* pressed, as defined at:
|
||||
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
|
||||
* @param {String} key The standard name of the key pressed, as defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
* @param {Number} location The location on the keyboard corresponding to
|
||||
* the key pressed, as defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
* @param {!KeyboardEvent} orig
|
||||
* The relevant DOM "keydown" event.
|
||||
*/
|
||||
var KeydownEvent = function(keyCode, keyIdentifier, key, location) {
|
||||
var KeydownEvent = function KeydownEvent(orig) {
|
||||
|
||||
// We extend KeyEvent
|
||||
KeyEvent.apply(this);
|
||||
|
||||
/**
|
||||
* The JavaScript key code of the key pressed.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
this.keyCode = keyCode;
|
||||
|
||||
/**
|
||||
* The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
|
||||
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.keyIdentifier = keyIdentifier;
|
||||
|
||||
/**
|
||||
* The standard name of the key pressed, as defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.key = key;
|
||||
|
||||
/**
|
||||
* The location on the keyboard corresponding to the key pressed, as
|
||||
* defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
this.location = location;
|
||||
KeyEvent.call(this, orig);
|
||||
|
||||
// If key is known from keyCode or DOM3 alone, use that
|
||||
this.keysym = keysym_from_key_identifier(key, location)
|
||||
|| keysym_from_keycode(keyCode, location);
|
||||
this.keysym = keysym_from_key_identifier(this.key, this.location)
|
||||
|| keysym_from_keycode(this.keyCode, this.location);
|
||||
|
||||
/**
|
||||
* Whether the keyup following this keydown event is known to be
|
||||
* reliable. If false, we cannot rely on the keyup event to occur.
|
||||
*
|
||||
* @type {Boolean}
|
||||
* @type {!boolean}
|
||||
*/
|
||||
this.keyupReliable = !quirks.keyupUnreliable;
|
||||
|
||||
@ -264,12 +282,12 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
this.reliable = true;
|
||||
|
||||
// Use legacy keyIdentifier as a last resort, if it looks sane
|
||||
if (!this.keysym && key_identifier_sane(keyCode, keyIdentifier))
|
||||
this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift);
|
||||
if (!this.keysym && key_identifier_sane(this.keyCode, this.keyIdentifier))
|
||||
this.keysym = keysym_from_key_identifier(this.keyIdentifier, this.location, this.modifiers.shift);
|
||||
|
||||
// If a key is pressed while meta is held down, the keyup will
|
||||
// never be sent in Chrome (bug #108404)
|
||||
if (guac_keyboard.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
|
||||
if (this.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
|
||||
this.keyupReliable = false;
|
||||
|
||||
// We cannot rely on receiving keyup for Caps Lock on certain platforms
|
||||
@ -277,21 +295,21 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
this.keyupReliable = false;
|
||||
|
||||
// Determine whether default action for Alt+combinations must be prevented
|
||||
var prevent_alt = !guac_keyboard.modifiers.ctrl && !quirks.altIsTypableOnly;
|
||||
var prevent_alt = !this.modifiers.ctrl && !quirks.altIsTypableOnly;
|
||||
|
||||
// Determine whether default action for Ctrl+combinations must be prevented
|
||||
var prevent_ctrl = !guac_keyboard.modifiers.alt;
|
||||
var prevent_ctrl = !this.modifiers.alt;
|
||||
|
||||
// We must rely on the (potentially buggy) keyIdentifier if preventing
|
||||
// the default action is important
|
||||
if ((prevent_ctrl && guac_keyboard.modifiers.ctrl)
|
||||
|| (prevent_alt && guac_keyboard.modifiers.alt)
|
||||
|| guac_keyboard.modifiers.meta
|
||||
|| guac_keyboard.modifiers.hyper)
|
||||
if ((prevent_ctrl && this.modifiers.ctrl)
|
||||
|| (prevent_alt && this.modifiers.alt)
|
||||
|| this.modifiers.meta
|
||||
|| this.modifiers.hyper)
|
||||
this.reliable = true;
|
||||
|
||||
// Record most recently known keysym by associated key code
|
||||
recentKeysym[keyCode] = this.keysym;
|
||||
recentKeysym[this.keyCode] = this.keysym;
|
||||
|
||||
};
|
||||
|
||||
@ -305,24 +323,16 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* @private
|
||||
* @constructor
|
||||
* @augments Guacamole.Keyboard.KeyEvent
|
||||
* @param {Number} charCode The Unicode codepoint of the character that
|
||||
* would be typed by the key pressed.
|
||||
* @param {!KeyboardEvent} orig
|
||||
* The relevant DOM "keypress" event.
|
||||
*/
|
||||
var KeypressEvent = function(charCode) {
|
||||
var KeypressEvent = function KeypressEvent(orig) {
|
||||
|
||||
// We extend KeyEvent
|
||||
KeyEvent.apply(this);
|
||||
|
||||
/**
|
||||
* The Unicode codepoint of the character that would be typed by the
|
||||
* key pressed.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
this.charCode = charCode;
|
||||
KeyEvent.call(this, orig);
|
||||
|
||||
// Pull keysym from char code
|
||||
this.keysym = keysym_from_charcode(charCode);
|
||||
this.keysym = keysym_from_charcode(this.keyCode);
|
||||
|
||||
// Keypress is always reliable
|
||||
this.reliable = true;
|
||||
@ -332,68 +342,30 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
KeypressEvent.prototype = new KeyEvent();
|
||||
|
||||
/**
|
||||
* Information related to the pressing of a key, which need not be a key
|
||||
* Information related to the releasing of a key, which need not be a key
|
||||
* associated with a printable character. The presence or absence of any
|
||||
* information within this object is browser-dependent.
|
||||
*
|
||||
* @private
|
||||
* @constructor
|
||||
* @augments Guacamole.Keyboard.KeyEvent
|
||||
* @param {Number} keyCode The JavaScript key code of the key released.
|
||||
* @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key
|
||||
* released, as defined at:
|
||||
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
|
||||
* @param {String} key The standard name of the key released, as defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
* @param {Number} location The location on the keyboard corresponding to
|
||||
* the key released, as defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
* @param {!KeyboardEvent} orig
|
||||
* The relevant DOM "keyup" event.
|
||||
*/
|
||||
var KeyupEvent = function(keyCode, keyIdentifier, key, location) {
|
||||
var KeyupEvent = function KeyupEvent(orig) {
|
||||
|
||||
// We extend KeyEvent
|
||||
KeyEvent.apply(this);
|
||||
KeyEvent.call(this, orig);
|
||||
|
||||
/**
|
||||
* The JavaScript key code of the key released.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
this.keyCode = keyCode;
|
||||
|
||||
/**
|
||||
* The legacy DOM3 "keyIdentifier" of the key released, as defined at:
|
||||
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.keyIdentifier = keyIdentifier;
|
||||
|
||||
/**
|
||||
* The standard name of the key released, as defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.key = key;
|
||||
|
||||
/**
|
||||
* The location on the keyboard corresponding to the key released, as
|
||||
* defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
this.location = location;
|
||||
|
||||
// If key is known from keyCode or DOM3 alone, use that
|
||||
this.keysym = keysym_from_keycode(keyCode, location)
|
||||
|| keysym_from_key_identifier(key, location); // keyCode is still more reliable for keyup when dead keys are in use
|
||||
// If key is known from keyCode or DOM3 alone, use that (keyCode is
|
||||
// still more reliable for keyup when dead keys are in use)
|
||||
this.keysym = keysym_from_keycode(this.keyCode, this.location)
|
||||
|| keysym_from_key_identifier(this.key, this.location);
|
||||
|
||||
// Fall back to the most recently pressed keysym associated with the
|
||||
// keyCode if the inferred key doesn't seem to actually be pressed
|
||||
if (!guac_keyboard.pressed[this.keysym])
|
||||
this.keysym = recentKeysym[keyCode] || this.keysym;
|
||||
this.keysym = recentKeysym[this.keyCode] || this.keysym;
|
||||
|
||||
// Keyup is as reliable as it will ever be
|
||||
this.reliable = true;
|
||||
@ -407,14 +379,16 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* KeydownEvent, KeypressEvent, and KeyupEvent classes.
|
||||
*
|
||||
* @private
|
||||
* @type {KeyEvent[]}
|
||||
* @type {!KeyEvent[]}
|
||||
*/
|
||||
var eventLog = [];
|
||||
|
||||
/**
|
||||
* Map of known JavaScript keycodes which do not map to typable characters
|
||||
* to their X11 keysym equivalents.
|
||||
*
|
||||
* @private
|
||||
* @type {!Object.<number, number[]>}
|
||||
*/
|
||||
var keycodeKeysyms = {
|
||||
8: [0xFF08], // backspace
|
||||
@ -438,9 +412,9 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2
|
||||
45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0
|
||||
46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal
|
||||
91: [0xFFEB], // left window key (hyper_l)
|
||||
92: [0xFF67], // right window key (menu key?)
|
||||
93: null, // select key
|
||||
91: [0xFFE7], // left windows/command key (meta_l)
|
||||
92: [0xFFE8], // right window/command key (meta_r)
|
||||
93: [0xFF67], // menu key
|
||||
96: [0xFFB0], // KP 0
|
||||
97: [0xFFB1], // KP 1
|
||||
98: [0xFFB2], // KP 2
|
||||
@ -476,7 +450,9 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
/**
|
||||
* Map of known JavaScript keyidentifiers which do not map to typable
|
||||
* characters to their unshifted X11 keysym equivalents.
|
||||
*
|
||||
* @private
|
||||
* @type {!Object.<string, number[]>}
|
||||
*/
|
||||
var keyidentifier_keysym = {
|
||||
"Again": [0xFF66],
|
||||
@ -584,14 +560,16 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
"UIKeyInputUpArrow": [0xFF52],
|
||||
"Up": [0xFF52],
|
||||
"Undo": [0xFF65],
|
||||
"Win": [0xFFEB],
|
||||
"Win": [0xFFE7, 0xFFE7, 0xFFE8],
|
||||
"Zenkaku": [0xFF28],
|
||||
"ZenkakuHankaku": [0xFF2A]
|
||||
};
|
||||
|
||||
/**
|
||||
* All keysyms which should not repeat when held down.
|
||||
*
|
||||
* @private
|
||||
* @type {!Object.<number, boolean>}
|
||||
*/
|
||||
var no_repeat = {
|
||||
0xFE03: true, // ISO Level 3 Shift (AltGr)
|
||||
@ -604,12 +582,14 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
0xFFE8: true, // Right meta
|
||||
0xFFE9: true, // Left alt
|
||||
0xFFEA: true, // Right alt
|
||||
0xFFEB: true, // Left hyper
|
||||
0xFFEC: true // Right hyper
|
||||
0xFFEB: true, // Left super/hyper
|
||||
0xFFEC: true // Right super/hyper
|
||||
};
|
||||
|
||||
/**
|
||||
* All modifiers and their states.
|
||||
*
|
||||
* @type {!Guacamole.Keyboard.ModifierState}
|
||||
*/
|
||||
this.modifiers = new Guacamole.Keyboard.ModifierState();
|
||||
|
||||
@ -617,6 +597,8 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* The state of every key, indexed by keysym. If a particular key is
|
||||
* pressed, the value of pressed for that keysym will be true. If a key
|
||||
* is not currently pressed, it will not be defined.
|
||||
*
|
||||
* @type {!Object.<number, boolean>}
|
||||
*/
|
||||
this.pressed = {};
|
||||
|
||||
@ -629,7 +611,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* of the key is explicitly known), it will not be defined.
|
||||
*
|
||||
* @private
|
||||
* @tyle {Object.<Number, Boolean>}
|
||||
* @type {!Object.<number, boolean>}
|
||||
*/
|
||||
var implicitlyPressed = {};
|
||||
|
||||
@ -640,6 +622,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* is (theoretically) still pressed.
|
||||
*
|
||||
* @private
|
||||
* @type {!Object.<number, boolean>}
|
||||
*/
|
||||
var last_keydown_result = {};
|
||||
|
||||
@ -648,20 +631,24 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* fired. This object maps keycodes to keysyms.
|
||||
*
|
||||
* @private
|
||||
* @type {Object.<Number, Number>}
|
||||
* @type {!Object.<number, number>}
|
||||
*/
|
||||
var recentKeysym = {};
|
||||
|
||||
/**
|
||||
* Timeout before key repeat starts.
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
var key_repeat_timeout = null;
|
||||
|
||||
/**
|
||||
* Interval which presses and releases the last key pressed while that
|
||||
* key is still being held down.
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
var key_repeat_interval = null;
|
||||
|
||||
@ -671,11 +658,11 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* undefined.
|
||||
*
|
||||
* @private
|
||||
* @param {Number[]} keysyms
|
||||
* @param {number[]} keysyms
|
||||
* An array of keysyms, where the index of the keysym in the array is
|
||||
* the location value.
|
||||
*
|
||||
* @param {Number} location
|
||||
* @param {!number} location
|
||||
* The location on the keyboard corresponding to the key pressed, as
|
||||
* defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
*/
|
||||
@ -691,10 +678,10 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* Returns true if the given keysym corresponds to a printable character,
|
||||
* false otherwise.
|
||||
*
|
||||
* @param {Number} keysym
|
||||
* @param {!number} keysym
|
||||
* The keysym to check.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* @returns {!boolean}
|
||||
* true if the given keysym corresponds to a printable character,
|
||||
* false otherwise.
|
||||
*/
|
||||
@ -773,13 +760,13 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* correct in all cases.
|
||||
*
|
||||
* @private
|
||||
* @param {Number} keyCode
|
||||
* @param {!number} keyCode
|
||||
* The keyCode from a browser keydown/keyup event.
|
||||
*
|
||||
* @param {String} keyIdentifier
|
||||
* @param {string} keyIdentifier
|
||||
* The legacy keyIdentifier from a browser keydown/keyup event.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* @returns {!boolean}
|
||||
* true if the keyIdentifier looks sane, false if the keyIdentifier
|
||||
* appears incorrectly derived or is missing entirely.
|
||||
*/
|
||||
@ -816,8 +803,11 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* not a modifier. The return value of this function depends on the
|
||||
* return value of the keydown event handler, if any.
|
||||
*
|
||||
* @param {Number} keysym The keysym of the key to press.
|
||||
* @return {Boolean} true if event should NOT be canceled, false otherwise.
|
||||
* @param {number} keysym
|
||||
* The keysym of the key to press.
|
||||
*
|
||||
* @return {boolean}
|
||||
* true if event should NOT be canceled, false otherwise.
|
||||
*/
|
||||
this.press = function(keysym) {
|
||||
|
||||
@ -860,7 +850,8 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
/**
|
||||
* Marks a key as released, firing the keyup event if registered.
|
||||
*
|
||||
* @param {Number} keysym The keysym of the key to release.
|
||||
* @param {number} keysym
|
||||
* The keysym of the key to release.
|
||||
*/
|
||||
this.release = function(keysym) {
|
||||
|
||||
@ -887,7 +878,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* Presses and releases the keys necessary to type the given string of
|
||||
* text.
|
||||
*
|
||||
* @param {String} str
|
||||
* @param {!string} str
|
||||
* The string to type.
|
||||
*/
|
||||
this.type = function type(str) {
|
||||
@ -923,27 +914,28 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Given the remote and local state of a particular key, resynchronizes the
|
||||
* remote state of that key with the local state through pressing or
|
||||
* Resynchronizes the remote state of the given modifier with its
|
||||
* corresponding local modifier state, as dictated by
|
||||
* {@link KeyEvent#modifiers} within the given key event, by pressing or
|
||||
* releasing keysyms.
|
||||
*
|
||||
* @private
|
||||
* @param {Boolean} remoteState
|
||||
* Whether the key is currently pressed remotely.
|
||||
* @param {!string} modifier
|
||||
* The name of the {@link Guacamole.Keyboard.ModifierState} property
|
||||
* being updated.
|
||||
*
|
||||
* @param {Boolean} localState
|
||||
* Whether the key is currently pressed remotely locally. If the state
|
||||
* of the key is not known, this may be undefined.
|
||||
* @param {!number[]} keysyms
|
||||
* The keysyms which represent the modifier being updated.
|
||||
*
|
||||
* @param {Number[]} keysyms
|
||||
* The keysyms which represent the key being updated.
|
||||
*
|
||||
* @param {KeyEvent} keyEvent
|
||||
* @param {!KeyEvent} keyEvent
|
||||
* Guacamole's current best interpretation of the key event being
|
||||
* processed.
|
||||
*/
|
||||
var updateModifierState = function updateModifierState(remoteState,
|
||||
localState, keysyms, keyEvent) {
|
||||
var updateModifierState = function updateModifierState(modifier,
|
||||
keysyms, keyEvent) {
|
||||
|
||||
var localState = keyEvent.modifiers[modifier];
|
||||
var remoteState = guac_keyboard.modifiers[modifier];
|
||||
|
||||
var i;
|
||||
|
||||
@ -988,56 +980,50 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a keyboard event, updates the local modifier state and remote
|
||||
* key state based on the modifier flags within the event. This function
|
||||
* pays no attention to keycodes.
|
||||
* Given a keyboard event, updates the remote key state to match the local
|
||||
* modifier state and remote based on the modifier flags within the event.
|
||||
* This function pays no attention to keycodes.
|
||||
*
|
||||
* @private
|
||||
* @param {KeyboardEvent} e
|
||||
* The keyboard event containing the flags to update.
|
||||
*
|
||||
* @param {KeyEvent} keyEvent
|
||||
* @param {!KeyEvent} keyEvent
|
||||
* Guacamole's current best interpretation of the key event being
|
||||
* processed.
|
||||
*/
|
||||
var syncModifierStates = function syncModifierStates(e, keyEvent) {
|
||||
|
||||
// Get state
|
||||
var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e);
|
||||
var syncModifierStates = function syncModifierStates(keyEvent) {
|
||||
|
||||
// Resync state of alt
|
||||
updateModifierState(guac_keyboard.modifiers.alt, state.alt, [
|
||||
updateModifierState('alt', [
|
||||
0xFFE9, // Left alt
|
||||
0xFFEA, // Right alt
|
||||
0xFE03 // AltGr
|
||||
], keyEvent);
|
||||
|
||||
// Resync state of shift
|
||||
updateModifierState(guac_keyboard.modifiers.shift, state.shift, [
|
||||
updateModifierState('shift', [
|
||||
0xFFE1, // Left shift
|
||||
0xFFE2 // Right shift
|
||||
], keyEvent);
|
||||
|
||||
// Resync state of ctrl
|
||||
updateModifierState(guac_keyboard.modifiers.ctrl, state.ctrl, [
|
||||
updateModifierState('ctrl', [
|
||||
0xFFE3, // Left ctrl
|
||||
0xFFE4 // Right ctrl
|
||||
], keyEvent);
|
||||
|
||||
// Resync state of meta
|
||||
updateModifierState(guac_keyboard.modifiers.meta, state.meta, [
|
||||
updateModifierState('meta', [
|
||||
0xFFE7, // Left meta
|
||||
0xFFE8 // Right meta
|
||||
], keyEvent);
|
||||
|
||||
// Resync state of hyper
|
||||
updateModifierState(guac_keyboard.modifiers.hyper, state.hyper, [
|
||||
0xFFEB, // Left hyper
|
||||
0xFFEC // Right hyper
|
||||
updateModifierState('hyper', [
|
||||
0xFFEB, // Left super/hyper
|
||||
0xFFEC // Right super/hyper
|
||||
], keyEvent);
|
||||
|
||||
// Update state
|
||||
guac_keyboard.modifiers = state;
|
||||
guac_keyboard.modifiers = keyEvent.modifiers;
|
||||
|
||||
};
|
||||
|
||||
@ -1047,7 +1033,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* inspection of other key events.
|
||||
*
|
||||
* @private
|
||||
* @returns {Boolean}
|
||||
* @returns {!boolean}
|
||||
* true if all currently pressed keys were implicitly pressed, false
|
||||
* otherwise.
|
||||
*/
|
||||
@ -1068,8 +1054,8 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* can be).
|
||||
*
|
||||
* @private
|
||||
* @return {Boolean} Whether the default action of the latest event should
|
||||
* be prevented.
|
||||
* @return {boolean}
|
||||
* Whether the default action of the latest event should be prevented.
|
||||
*/
|
||||
function interpret_events() {
|
||||
|
||||
@ -1099,7 +1085,8 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* looks like a key that may require AltGr.
|
||||
*
|
||||
* @private
|
||||
* @param {Number} keysym The key that was just pressed.
|
||||
* @param {!number} keysym
|
||||
* The key that was just pressed.
|
||||
*/
|
||||
var release_simulated_altgr = function release_simulated_altgr(keysym) {
|
||||
|
||||
@ -1149,6 +1136,29 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
var keysym = null;
|
||||
var accepted_events = [];
|
||||
|
||||
// Defer handling of Meta until it is known to be functioning as a
|
||||
// modifier (it may otherwise actually be an alternative method for
|
||||
// pressing a single key, such as Meta+Left for Home on ChromeOS)
|
||||
if (first.keysym === 0xFFE7 || first.keysym === 0xFFE8) {
|
||||
|
||||
// Defer handling until further events exist to provide context
|
||||
if (eventLog.length === 1)
|
||||
return null;
|
||||
|
||||
// Drop keydown if it turns out Meta does not actually apply
|
||||
if (eventLog[1].keysym !== first.keysym) {
|
||||
if (!eventLog[1].modifiers.meta)
|
||||
return eventLog.shift();
|
||||
}
|
||||
|
||||
// Drop duplicate keydown events while waiting to determine
|
||||
// whether to acknowledge Meta (browser may repeat keydown
|
||||
// while the key is held)
|
||||
else if (eventLog[1] instanceof KeydownEvent)
|
||||
return eventLog.shift();
|
||||
|
||||
}
|
||||
|
||||
// If event itself is reliable, no need to wait for other events
|
||||
if (first.reliable) {
|
||||
keysym = first.keysym;
|
||||
@ -1172,6 +1182,8 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
// Fire a key press if valid events were found
|
||||
if (accepted_events.length > 0) {
|
||||
|
||||
syncModifierStates(first);
|
||||
|
||||
if (keysym) {
|
||||
|
||||
// Fire event
|
||||
@ -1213,6 +1225,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
return first;
|
||||
}
|
||||
|
||||
syncModifierStates(first);
|
||||
return eventLog.shift();
|
||||
|
||||
} // end if keyup
|
||||
@ -1233,11 +1246,11 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* have the same keycode, such as left shift vs. right shift.
|
||||
*
|
||||
* @private
|
||||
* @param {KeyboardEvent} e
|
||||
* @param {!KeyboardEvent} e
|
||||
* A JavaScript keyboard event, as received through the DOM via a
|
||||
* "keydown", "keyup", or "keypress" handler.
|
||||
*
|
||||
* @returns {Number}
|
||||
* @returns {!number}
|
||||
* The location of the key event on the keyboard, as defined at:
|
||||
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
|
||||
*/
|
||||
@ -1261,10 +1274,10 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* Guacamole.Keyboard. If the Event has already been marked as handled,
|
||||
* false is returned.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {!Event} e
|
||||
* The Event to mark.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* @returns {!boolean}
|
||||
* true if the given Event was successfully marked, false if the given
|
||||
* Event was already marked.
|
||||
*/
|
||||
@ -1286,7 +1299,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* events signalled through this Guacamole.Keyboard's onkeydown and
|
||||
* onkeyup handlers.
|
||||
*
|
||||
* @param {Element|Document} element
|
||||
* @param {!(Element|Document)} element
|
||||
* The Element to attach event listeners to for the sake of handling
|
||||
* key or input events.
|
||||
*/
|
||||
@ -1301,17 +1314,11 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
// Ignore events which have already been handled
|
||||
if (!markEvent(e)) return;
|
||||
|
||||
var keyCode;
|
||||
if (window.event) keyCode = window.event.keyCode;
|
||||
else if (e.which) keyCode = e.which;
|
||||
|
||||
// Fix modifier states
|
||||
var keydownEvent = new KeydownEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e));
|
||||
syncModifierStates(e, keydownEvent);
|
||||
var keydownEvent = new KeydownEvent(e);
|
||||
|
||||
// Ignore (but do not prevent) the "composition" keycode sent by some
|
||||
// browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html)
|
||||
if (keyCode === 229)
|
||||
if (keydownEvent.keyCode === 229)
|
||||
return;
|
||||
|
||||
// Log event
|
||||
@ -1332,16 +1339,8 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
// Ignore events which have already been handled
|
||||
if (!markEvent(e)) return;
|
||||
|
||||
var charCode;
|
||||
if (window.event) charCode = window.event.keyCode;
|
||||
else if (e.which) charCode = e.which;
|
||||
|
||||
// Fix modifier states
|
||||
var keypressEvent = new KeypressEvent(charCode);
|
||||
syncModifierStates(e, keypressEvent);
|
||||
|
||||
// Log event
|
||||
eventLog.push(keypressEvent);
|
||||
eventLog.push(new KeypressEvent(e));
|
||||
|
||||
// Interpret as many events as possible, prevent default if indicated
|
||||
if (interpret_events())
|
||||
@ -1360,74 +1359,13 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
var keyCode;
|
||||
if (window.event) keyCode = window.event.keyCode;
|
||||
else if (e.which) keyCode = e.which;
|
||||
|
||||
// Fix modifier states
|
||||
var keyupEvent = new KeyupEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e));
|
||||
syncModifierStates(e, keyupEvent);
|
||||
|
||||
// Log event, call for interpretation
|
||||
eventLog.push(keyupEvent);
|
||||
eventLog.push(new KeyupEvent(e));
|
||||
interpret_events();
|
||||
|
||||
}, true);
|
||||
|
||||
/**
|
||||
* Handles the given "input" event, typing the data within the input text.
|
||||
* If the event is complete (text is provided), handling of "compositionend"
|
||||
* events is suspended, as such events may conflict with input events.
|
||||
*
|
||||
* @private
|
||||
* @param {InputEvent} e
|
||||
* The "input" event to handle.
|
||||
*/
|
||||
var handleInput = function handleInput(e) {
|
||||
|
||||
// Only intercept if handler set
|
||||
if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
|
||||
|
||||
// Ignore events which have already been handled
|
||||
if (!markEvent(e)) return;
|
||||
|
||||
// Type all content written
|
||||
if (e.data && !e.isComposing) {
|
||||
element.removeEventListener("compositionend", handleComposition, false);
|
||||
guac_keyboard.type(e.data);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the given "compositionend" event, typing the data within the
|
||||
* composed text. If the event is complete (composed text is provided),
|
||||
* handling of "input" events is suspended, as such events may conflict
|
||||
* with composition events.
|
||||
*
|
||||
* @private
|
||||
* @param {CompositionEvent} e
|
||||
* The "compositionend" event to handle.
|
||||
*/
|
||||
var handleComposition = function handleComposition(e) {
|
||||
|
||||
// Only intercept if handler set
|
||||
if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
|
||||
|
||||
// Ignore events which have already been handled
|
||||
if (!markEvent(e)) return;
|
||||
|
||||
// Type all content written
|
||||
if (e.data) {
|
||||
element.removeEventListener("input", handleInput, false);
|
||||
guac_keyboard.type(e.data);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Automatically type text entered into the wrapped field
|
||||
element.addEventListener("input", handleInput, false);
|
||||
element.addEventListener("compositionend", handleComposition, false);
|
||||
// NEKO: Do not automatically type text entered into the wrapped field
|
||||
|
||||
};
|
||||
|
||||
@ -1442,7 +1380,7 @@ Guacamole.Keyboard = function Keyboard(element) {
|
||||
* instance.
|
||||
*
|
||||
* @private
|
||||
* @type {Number}
|
||||
* @type {!number}
|
||||
*/
|
||||
Guacamole.Keyboard._nextID = 0;
|
||||
|
||||
@ -1454,31 +1392,36 @@ Guacamole.Keyboard.ModifierState = function() {
|
||||
|
||||
/**
|
||||
* Whether shift is currently pressed.
|
||||
* @type {Boolean}
|
||||
*
|
||||
* @type {!boolean}
|
||||
*/
|
||||
this.shift = false;
|
||||
|
||||
/**
|
||||
* Whether ctrl is currently pressed.
|
||||
* @type {Boolean}
|
||||
*
|
||||
* @type {!boolean}
|
||||
*/
|
||||
this.ctrl = false;
|
||||
|
||||
/**
|
||||
* Whether alt is currently pressed.
|
||||
* @type {Boolean}
|
||||
*
|
||||
* @type {!boolean}
|
||||
*/
|
||||
this.alt = false;
|
||||
|
||||
/**
|
||||
* Whether meta (apple key) is currently pressed.
|
||||
* @type {Boolean}
|
||||
*
|
||||
* @type {!boolean}
|
||||
*/
|
||||
this.meta = false;
|
||||
|
||||
/**
|
||||
* Whether hyper (windows key) is currently pressed.
|
||||
* @type {Boolean}
|
||||
*
|
||||
* @type {!boolean}
|
||||
*/
|
||||
this.hyper = false;
|
||||
|
||||
@ -1487,9 +1430,11 @@ Guacamole.Keyboard.ModifierState = function() {
|
||||
/**
|
||||
* Returns the modifier state applicable to the keyboard event given.
|
||||
*
|
||||
* @param {KeyboardEvent} e The keyboard event to read.
|
||||
* @returns {Guacamole.Keyboard.ModifierState} The current state of keyboard
|
||||
* modifiers.
|
||||
* @param {!KeyboardEvent} e
|
||||
* The keyboard event to read.
|
||||
*
|
||||
* @returns {!Guacamole.Keyboard.ModifierState}
|
||||
* The current state of keyboard modifiers.
|
||||
*/
|
||||
Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) {
|
||||
|
||||
|
@ -7,3 +7,58 @@ export function makeid(length: number) {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function elementRequestFullscreen(el: HTMLElement) {
|
||||
if (typeof el.requestFullscreen === 'function') {
|
||||
el.requestFullscreen()
|
||||
//@ts-ignore
|
||||
} else if (typeof el.webkitRequestFullscreen === 'function') {
|
||||
//@ts-ignore
|
||||
el.webkitRequestFullscreen()
|
||||
//@ts-ignore
|
||||
} else if (typeof el.webkitEnterFullscreen === 'function') {
|
||||
//@ts-ignore
|
||||
el.webkitEnterFullscreen()
|
||||
//@ts-ignore
|
||||
} else if (typeof el.mozRequestFullScreen === 'function') {
|
||||
//@ts-ignore
|
||||
el.mozRequestFullScreen()
|
||||
//@ts-ignore
|
||||
} else if (typeof el.msRequestFullScreen === 'function') {
|
||||
//@ts-ignore
|
||||
el.msRequestFullScreen()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function isFullscreen(): boolean {
|
||||
return (
|
||||
document.fullscreenElement ||
|
||||
//@ts-ignore
|
||||
document.msFullscreenElement ||
|
||||
//@ts-ignore
|
||||
document.mozFullScreenElement ||
|
||||
//@ts-ignore
|
||||
document.webkitFullscreenElement
|
||||
)
|
||||
}
|
||||
|
||||
export function onFullscreenChange(el: HTMLElement, fn: () => void) {
|
||||
if (el.onfullscreenchange === null) {
|
||||
el.onfullscreenchange = fn
|
||||
//@ts-ignore
|
||||
} else if (el.onmsfullscreenchange === null) {
|
||||
//@ts-ignore
|
||||
el.onmsfullscreenchange = fn
|
||||
//@ts-ignore
|
||||
} else if (el.onmozfullscreenchange === null) {
|
||||
//@ts-ignore
|
||||
el.onmozfullscreenchange = fn
|
||||
//@ts-ignore
|
||||
} else if (el.onwebkitfullscreenchange === null) {
|
||||
//@ts-ignore
|
||||
el.onwebkitfullscreenchange = fn
|
||||
}
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ module.exports = {
|
||||
},
|
||||
devServer: {
|
||||
disableHostCheck: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
<div align="center">
|
||||
<a href="https://neko.m1k1o.net/#/" ><img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="450" height="auto"/></a>
|
||||
<a href="https://neko.m1k1o.net/#/" ><img src="./_media/logo.png" width="450" height="auto"/></a>
|
||||
<br/>
|
||||
<br/>
|
||||
<img src="https://i.imgur.com/ZSzbQr7.gif" width="650" height="auto"/>
|
||||
<img src="./_media/intro.gif" width="650" height="auto"/>
|
||||
|
||||
With `NEKO_FILE_TRANSFER_ENABLED=true`:
|
||||
|
||||
<img src="./_media/file-transfer.gif" width="650" height="auto"/>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
BIN
docs/_media/file-transfer.gif
Normal file
After Width: | Height: | Size: 2.9 MiB |
4
docs/_media/icons/brave.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="512" cy="512" r="512" style="fill:#fb542b"/>
|
||||
<path d="M586.5 266.2c3 0 5.9 1.3 7.8 3.6l38.4 44.3 1.5-.3c9.2-1.6 18.8-2.2 27.9-1.2 11.7 1.4 21.7 5.5 29.1 13 7.8 7.9 15.5 16 22.8 23.8 2.6 2.7 4.9 5.2 7 7.5.7.8 1.4 1.5 1.9 2.1 3.4 3.8 4.2 8.2 2.7 12l-9.9 24.8 13.2 38.4c.7 2 .8 4.2.2 6.2-.1.4-.1.4-.5 2.1-.6 2.4-.6 2.4-1.5 5.9-1.6 6.3-3.5 13.4-5.4 21-5.6 21.8-11.3 43.6-16.5 63.9-13 50.2-21.5 83.3-23.5 91.6-11.2 44.9-20 60.5-48.6 81-25.1 18-77.4 54-87.4 60.4-1 .6-2 1.3-3.4 2.3-.5.4-3.2 2.2-4 2.7-4.9 3.4-8.4 5.5-12.1 7.3-4.8 2.2-9.3 3.5-14 3.5-4.6 0-9.2-1.3-14-3.5-3.8-1.8-7.2-3.9-12.1-7.3-.8-.5-3.4-2.4-4-2.7-1.4-1-2.5-1.7-3.4-2.3-10.1-6.4-62.4-42.5-87.4-60.4-28.6-20.5-37.5-36.1-48.6-81a49038 49038 0 0 0-23.5-91.2c-5.4-20.7-11-42.5-16.6-64.3-2-7.6-3.8-14.7-5.5-21-.9-3.5-.9-3.5-1.5-5.9-.4-1.7-.4-1.7-.5-2.1-.5-2.1-.4-4.2.2-6.2l13.2-38.4-9.9-24.8c-1.5-3.8-.7-8.2 2-11.2 1.2-1.3 1.9-2 2.6-2.8 2.1-2.2 4.4-4.7 7-7.5 7.3-7.8 15-15.9 22.8-23.8 7.4-7.6 17.4-11.6 29.1-13 9.1-1.1 18.7-.5 27.9 1.2l1.5.3 38.4-44.3c2-2.3 4.8-3.6 7.8-3.6h148.8zm-4.7 21.2H442.2l-39.3 45.5c-2.6 3.1-6.8 4.3-10.6 3.2-.2-.1-.7-.2-1.5-.4-1.3-.3-2.9-.6-4.6-.9-7.4-1.3-15-1.8-21.9-1-7.4.9-13.1 3.2-16.8 7-7.6 7.8-15.3 15.7-22.5 23.4-1.7 1.8-3.3 3.5-4.8 5.1l8.9 22.2c1 2.4 1 5.1.2 7.5l-13.4 39.1c.4 1.4.5 1.9 1.3 4.8 1.6 6.3 3.5 13.4 5.4 21 5.6 21.8 11.3 43.6 16.5 63.9 13 50.4 21.5 83.4 23.6 91.7 10 40.4 16.4 51.6 40.4 68.8 24.8 17.8 76.8 53.6 86.5 59.8 1.2.8 2.5 1.6 4.1 2.7.6.4 3.2 2.2 3.9 2.7 4 2.8 6.7 4.4 9.3 5.6 2.2 1.1 4 1.5 5.1 1.5 1.2 0 2.9-.5 5.1-1.5 2.5-1.2 5.2-2.9 9.2-5.6.7-.5 3.4-2.3 4-2.7 1.6-1.1 2.9-2 4.1-2.7 9.7-6.2 61.7-42 86.5-59.8 24-17.2 30.4-28.4 40.4-68.8 2.1-8.3 10.6-41.4 23.5-91.4 5.4-20.7 11-42.5 16.6-64.3 2-7.6 3.8-14.7 5.4-21 .8-2.9.9-3.4 1.3-4.8l-13.4-39c-.8-2.4-.8-5.1.2-7.5l8.9-22.2c-1.5-1.6-3.1-3.3-4.8-5.1-7.2-7.7-14.9-15.6-22.5-23.4-3.7-3.8-9.4-6.1-16.8-7-6.8-.8-14.5-.3-21.9 1-1.7.3-3.2.6-4.6.9-.8.2-1.3.3-1.5.4-3.9 1.1-8-.2-10.6-3.2l-39.3-45.5zM512 584.7c2.8 0 21 6.5 35.6 14.2 14.6 7.7 25.2 13.1 28.5 15.3 3.4 2.1 1.3 6.2-1.8 8.4s-44.5 34.9-48.5 38.5c-4 3.6-9.9 9.6-13.9 9.6s-9.9-6-13.9-9.6c-4-3.6-45.4-36.2-48.5-38.5-3.1-2.2-5.1-6.3-1.8-8.4 3.4-2.2 14-7.6 28.5-15.3 14.8-7.7 33-14.2 35.8-14.2m.1-232.5c.7 0 8.8.2 20.6 4.2 12.4 4.2 25.9 9.5 32.1 9.5s52.3-9 52.3-9 54.6 67.2 54.6 81.6c0 14.4-6.9 18.2-13.8 25.6-6.9 7.5-37.1 40.1-40.9 44.3-3.9 4.2-11.9 10.5-7.2 22 4.7 11.4 11.7 26 4 40.7-7.8 14.8-21.1 24.6-29.7 23-8.5-1.6-28.6-12.3-36-17.2-7.4-4.9-30.8-24.5-30.8-32s24.2-21 28.6-24.1c4.5-3.1 24.8-14.9 25.2-19.6.4-4.7.3-6-5.8-17.5-6-11.5-16.8-26.9-15-37.1 1.8-10.2 19.3-15.5 31.7-20.3 12.4-4.8 36.4-13.8 39.4-15.2 3-1.4 2.2-2.7-6.8-3.6-9.1-.9-34.8-4.4-46.4-1.1-11.6 3.3-31.4 8.3-33.1 11-1.6 2.7-3 2.7-1.4 11.9 1.7 9.2 10.1 53.2 11 61 .8 7.8 2.4 13-5.8 14.9-8.3 1.9-22.2 5.3-27 5.3-4.8 0-18.7-3.4-27-5.3s-6.7-7.1-5.8-14.9c.8-7.8 9.3-51.9 11-61 1.6-9.2.2-9.3-1.4-11.9-1.6-2.7-21.4-7.7-33.1-11-11.6-3.3-37.4.2-46.4 1.1-9.1.9-9.8 2.2-6.8 3.6 3 1.4 27 10.5 39.4 15.2 12.5 4.8 29.9 10.1 31.7 20.3s-9 25.6-15 37.1-6.2 12.9-5.8 17.5 20.8 16.5 25.2 19.6c4.5 3.1 28.6 16.6 28.6 24.1s-23.4 27.2-30.8 32c-7.4 4.9-27.4 15.6-36 17.2-8.5 1.6-21.9-8.2-29.7-23-7.8-14.8-.8-29.3 4-40.7s-3.3-17.8-7.2-22c-3.9-4.2-34-36.8-40.9-44.3-6.9-7.5-13.8-11.3-13.8-25.6 0-14.4 54.6-81.6 54.6-81.6s46.1 9 52.3 9c6.2 0 19.7-5.3 32.1-9.5 12.6-4.2 20.9-4.2 21-4.2z" style="fill:#fff"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
1
docs/_media/icons/chromium.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="190px" height="190px" viewBox="1 1 190 190" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="c"><circle cx="96" cy="96" r="88"/></clipPath><linearGradient id="d" x1="110.87" x2="52.54" y1="164.5" y2="130.33" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".4"/><stop offset=".33" stop-color="#1a237e" stop-opacity="0"/></linearGradient><linearGradient id="e" x1="30.43" x2="86.54" y1="74.4" y2="41.61" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".2"/><stop offset=".66" stop-color="#1a237e" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="118.57" x2="54.58" y1="169.11" y2="131.57" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".2"/><stop offset=".33" stop-color="#1a237e" stop-opacity="0"/></linearGradient><clipPath id="g"><use xlink:href="#R"/></clipPath><linearGradient id="a" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".3"/><stop offset=".66" stop-color="#1a237e" stop-opacity="0"/></linearGradient><linearGradient id="h" x1="121.86" x2="136.55" y1="49.8" y2="114.13" xlink:href="#a"/><linearGradient id="i" x1="121.87" x2="136.29" y1="50.07" y2="113.01" xlink:href="#a"/><clipPath id="j"><use xlink:href="#S"/></clipPath><linearGradient id="k" x1="29.34" x2="81.84" y1="75.02" y2="44.35" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".6"/><stop offset=".66" stop-color="#1a237e" stop-opacity="0"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".2"/><stop offset="1" stop-color="#1a237e" stop-opacity="0"/></linearGradient><radialGradient id="l" cx="92.18" cy="55.95" r="84.08" xlink:href="#b"/><clipPath id="n"><path d="M61.36 116L96 56h88V8H21.97v40.34"/></clipPath><linearGradient id="m" x1="8" x2="130.65" y1="104.24" y2="104.24" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f4b400" stop-opacity=".4"/><stop offset=".09" stop-color="#f2a700" stop-opacity=".3"/><stop offset=".22" stop-color="#f19800" stop-opacity=".13"/><stop offset=".33" stop-color="#f09300" stop-opacity="0"/></linearGradient><radialGradient id="o" cx="21.87" cy="48.52" r="78.04" xlink:href="#b"/><radialGradient id="p" cx="95.84" cy="96.14" r="87.87" xlink:href="#b"/><radialGradient id="q" cx="34.29" cy="32.01" r="176.75" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff" stop-opacity=".1"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient><g clip-path="url(#c)"><use fill="#5e97f6" xlink:href="#R"/><use fill="url(#d)" xlink:href="#R"/><use fill="#3367d6" xlink:href="#T"/><use fill="url(#e)" xlink:href="#T"/><path fill="#1a237e" fill-opacity=".15" d="M62.3 115.65l-39.83-68.3-.58 1 39.54 67.8z"/><use fill="#5e97f6" xlink:href="#R"/><use fill="url(#f)" xlink:href="#R"/><path fill="#1a237e" fill-opacity=".15" d="M129.84 117.33l-.83-.48L90.62 184h1.15l38.1-66.64z"/><g clip-path="url(#g)"><use fill="#a1c2fa" xlink:href="#S"/><use fill="url(#h)" xlink:href="#S"/></g><use fill="#a1c2fa" xlink:href="#S"/><use fill="url(#i)" xlink:href="#S"/><g clip-path="url(#j)"><use fill="#3367d6" xlink:href="#T"/><use fill="url(#k)" xlink:href="#T"/></g><path fill="url(#l)" d="M96 56v20.95L174.4 56z"/><use fill="url(#m)" clip-path="url(#n)" xlink:href="#R"/><path fill="url(#o)" d="M21.97 48.45l57.25 57.24L61.36 116z"/><path fill="url(#p)" d="M91.83 183.9l20.96-78.2 17.86 10.3z"/><circle cx="96" cy="96" r="40" fill="#f5f5f5"/><circle cx="96" cy="96" r="32" fill="#4285f4"/><path fill="#1a237e" fill-opacity=".2" d="M96 55a40 40 0 00-40 40v1a40 40 0 0140-40h88v-1z"/><path fill="#fff" fill-opacity=".1" d="M130.6 116A39.96 39.96 0 0196 136a39.97 39.97 0 01-34.61-20h-.04L8 24.48v1L61.36 117h.04a39.94 39.94 0 0069.21 0h.05v-1z"/><path fill="#1a237e" d="M97 56c-.17 0-.33.02-.5.03a39.98 39.98 0 010 79.94c.17 0 .33.03.5.03a40 40 0 000-80z" opacity=".1"/><path fill="#fff" fill-opacity=".2" d="M131 117.33a39.72 39.72 0 003.5-32.05 39.72 39.72 0 01-3.87 30.69l.02.04-38.87 68h1.16l38.1-66.64zM96 9a88 88 0 0187.99 87.5l.01-.5A88 88 0 008 96v.5A88 88 0 0196 9z"/><path fill="#1a237e" fill-opacity=".15" d="M96 183a88 88 0 0087.99-87.5l.01.5A88 88 0 018 96l.01-.5A88 88 0 0096 183z"/></g><circle cx="96" cy="96" r="88" fill="url(#q)"/><defs><path id="R" d="M8 184h83.77l38.88-38.88V116H61.36L8 24.48z"/><path id="S" d="M96 56l34.65 60-38.88 68H184V56z"/><path id="T" d="M21.97 8v108h39.4L96 56h88V8z"/></defs></svg>
|
After Width: | Height: | Size: 4.5 KiB |
111
docs/_media/icons/firefox.svg
Normal file
@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg width="77.42" height="79.97" version="1.1" viewBox="0 0 77.42 79.97" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Firefox Browser logo</title>
|
||||
<defs>
|
||||
<linearGradient id="a" x1="70.79" x2="6.447" y1="12.39" y2="74.47" gradientTransform="translate(-1.3 -.004086)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff44f" offset=".048"/>
|
||||
<stop stop-color="#ffe847" offset=".111"/>
|
||||
<stop stop-color="#ffc830" offset=".225"/>
|
||||
<stop stop-color="#ff980e" offset=".368"/>
|
||||
<stop stop-color="#ff8b16" offset=".401"/>
|
||||
<stop stop-color="#ff672a" offset=".462"/>
|
||||
<stop stop-color="#ff3647" offset=".534"/>
|
||||
<stop stop-color="#e31587" offset=".705"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="b" cx="-7907" cy="-8515" r="80.8" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#ffbd4f" offset=".129"/>
|
||||
<stop stop-color="#ffac31" offset=".186"/>
|
||||
<stop stop-color="#ff9d17" offset=".247"/>
|
||||
<stop stop-color="#ff980e" offset=".283"/>
|
||||
<stop stop-color="#ff563b" offset=".403"/>
|
||||
<stop stop-color="#ff3750" offset=".467"/>
|
||||
<stop stop-color="#f5156c" offset=".71"/>
|
||||
<stop stop-color="#eb0878" offset=".782"/>
|
||||
<stop stop-color="#e50080" offset=".86"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="c" cx="-7937" cy="-8482" r="80.8" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#960e18" offset=".3"/>
|
||||
<stop stop-color="#b11927" stop-opacity=".74" offset=".351"/>
|
||||
<stop stop-color="#db293d" stop-opacity=".343" offset=".435"/>
|
||||
<stop stop-color="#f5334b" stop-opacity=".094" offset=".497"/>
|
||||
<stop stop-color="#ff3750" stop-opacity="0" offset=".53"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="d" cx="-7927" cy="-8533" r="58.53" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff44f" offset=".132"/>
|
||||
<stop stop-color="#ffdc3e" offset=".252"/>
|
||||
<stop stop-color="#ff9d12" offset=".506"/>
|
||||
<stop stop-color="#ff980e" offset=".526"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="e" cx="-7946" cy="-8461" r="38.47" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3a8ee6" offset=".353"/>
|
||||
<stop stop-color="#5c79f0" offset=".472"/>
|
||||
<stop stop-color="#9059ff" offset=".669"/>
|
||||
<stop stop-color="#c139e6" offset="1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="f" cx="-7936" cy="-8492" r="20.4" gradientTransform="matrix(.972 -.235 .275 1.138 10090 7834)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9059ff" stop-opacity="0" offset=".206"/>
|
||||
<stop stop-color="#8c4ff3" stop-opacity=".064" offset=".278"/>
|
||||
<stop stop-color="#7716a8" stop-opacity=".45" offset=".747"/>
|
||||
<stop stop-color="#6e008b" stop-opacity=".6" offset=".975"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="g" cx="-7938" cy="-8518" r="27.68" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#ffe226" offset="0"/>
|
||||
<stop stop-color="#ffdb27" offset=".121"/>
|
||||
<stop stop-color="#ffc82a" offset=".295"/>
|
||||
<stop stop-color="#ffa930" offset=".502"/>
|
||||
<stop stop-color="#ff7e37" offset=".732"/>
|
||||
<stop stop-color="#ff7139" offset=".792"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="h" cx="-7916" cy="-8536" r="118.1" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff44f" offset=".113"/>
|
||||
<stop stop-color="#ff980e" offset=".456"/>
|
||||
<stop stop-color="#ff5634" offset=".622"/>
|
||||
<stop stop-color="#ff3647" offset=".716"/>
|
||||
<stop stop-color="#e31587" offset=".904"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="i" cx="-7927" cy="-8523" r="86.5" gradientTransform="matrix(.105 .995 -.653 .069 -4685 8470)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff44f" offset="0"/>
|
||||
<stop stop-color="#ffe847" offset=".06"/>
|
||||
<stop stop-color="#ffc830" offset=".168"/>
|
||||
<stop stop-color="#ff980e" offset=".304"/>
|
||||
<stop stop-color="#ff8b16" offset=".356"/>
|
||||
<stop stop-color="#ff672a" offset=".455"/>
|
||||
<stop stop-color="#ff3647" offset=".57"/>
|
||||
<stop stop-color="#e31587" offset=".737"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="j" cx="-7938" cy="-8508" r="73.72" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff44f" offset=".137"/>
|
||||
<stop stop-color="#ff980e" offset=".48"/>
|
||||
<stop stop-color="#ff5634" offset=".592"/>
|
||||
<stop stop-color="#ff3647" offset=".655"/>
|
||||
<stop stop-color="#e31587" offset=".904"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="k" cx="-7919" cy="-8504" r="80.69" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff44f" offset=".094"/>
|
||||
<stop stop-color="#ffe141" offset=".231"/>
|
||||
<stop stop-color="#ffaf1e" offset=".509"/>
|
||||
<stop stop-color="#ff980e" offset=".626"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="l" x1="70.01" x2="15.27" y1="12.06" y2="66.81" gradientTransform="translate(-1.3 -.004086)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff44f" stop-opacity=".8" offset=".167"/>
|
||||
<stop stop-color="#fff44f" stop-opacity=".634" offset=".266"/>
|
||||
<stop stop-color="#fff44f" stop-opacity=".217" offset=".489"/>
|
||||
<stop stop-color="#fff44f" stop-opacity="0" offset=".6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76l7e-3 0.065c-4.382-10.92-11.81-15.33-17.88-24.92-0.307-0.485-0.614-0.971-0.913-1.484-0.171-0.293-0.308-0.557-0.427-0.8a7.053 7.053 0 0 1-0.578-1.535 0.1 0.1 0 0 0-0.088-0.1 0.138 0.138 0 0 0-0.073 0c-5e-3 0-0.013 9e-3 -0.019 0.011s-0.019 0.011-0.028 0.015l0.015-0.026c-9.735 5.7-13.04 16.25-13.34 21.53a19.39 19.39 0 0 0-10.67 4.111 11.59 11.59 0 0 0-1-0.758 17.97 17.97 0 0 1-0.109-9.473 28.7 28.7 0 0 0-9.329 7.21h-0.018c-1.536-1.947-1.428-8.367-1.34-9.708a6.928 6.928 0 0 0-1.294 0.687 28.22 28.22 0 0 0-3.788 3.245 33.84 33.84 0 0 0-3.623 4.347v6e-3 -7e-3a32.73 32.73 0 0 0-5.2 11.74l-0.052 0.256c-0.073 0.341-0.336 2.049-0.381 2.42 0 0.029-6e-3 0.056-9e-3 0.085a36.94 36.94 0 0 0-0.629 5.343v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47zm-44.67 30.34c0.181 0.087 0.351 0.181 0.537 0.264l0.027 0.017q-0.282-0.135-0.564-0.281zm8.878-23.38m31.95-4.934v-0.037l7e-3 0.041z" style="fill:url(#a)"/>
|
||||
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76v0.037l7e-3 0.041a35.1 35.1 0 0 1-1.206 26.16c-4.442 9.531-15.19 19.3-32.02 18.82-18.18-0.515-34.2-14.01-37.19-31.68-0.545-2.787 0-4.2 0.274-6.465a28.88 28.88 0 0 0-0.623 5.348v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47z" style="fill:url(#b)"/>
|
||||
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76v0.037l7e-3 0.041a35.1 35.1 0 0 1-1.206 26.16c-4.442 9.531-15.19 19.3-32.02 18.82-18.18-0.515-34.2-14.01-37.19-31.68-0.545-2.787 0-4.2 0.274-6.465a28.88 28.88 0 0 0-0.623 5.348v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47z" style="fill:url(#c)"/>
|
||||
<path d="m55.78 31.38c0.084 0.059 0.162 0.118 0.241 0.177a21.1 21.1 0 0 0-3.6-4.695c-12.05-12.05-3.157-26.12-1.658-26.84l0.015-0.022c-9.735 5.7-13.04 16.25-13.34 21.53 0.452-0.031 0.9-0.069 1.362-0.069a19.56 19.56 0 0 1 16.98 9.917z" style="fill:url(#d)"/>
|
||||
<path d="m38.82 33.79c-0.064 0.964-3.47 4.289-4.661 4.289-11.02 0-12.81 6.667-12.81 6.667 0.488 5.614 4.4 10.24 9.129 12.68 0.216 0.112 0.435 0.213 0.654 0.312q0.569 0.252 1.138 0.466a17.24 17.24 0 0 0 5.043 0.973c19.32 0.906 23.06-23.1 9.119-30.07a13.38 13.38 0 0 1 9.345 2.269 19.56 19.56 0 0 0-16.98-9.917c-0.46 0-0.91 0.038-1.362 0.069a19.39 19.39 0 0 0-10.67 4.111c0.591 0.5 1.258 1.168 2.663 2.553 2.63 2.591 9.375 5.275 9.39 5.59z" style="fill:url(#e)"/>
|
||||
<path d="m38.82 33.79c-0.064 0.964-3.47 4.289-4.661 4.289-11.02 0-12.81 6.667-12.81 6.667 0.488 5.614 4.4 10.24 9.129 12.68 0.216 0.112 0.435 0.213 0.654 0.312q0.569 0.252 1.138 0.466a17.24 17.24 0 0 0 5.043 0.973c19.32 0.906 23.06-23.1 9.119-30.07a13.38 13.38 0 0 1 9.345 2.269 19.56 19.56 0 0 0-16.98-9.917c-0.46 0-0.91 0.038-1.362 0.069a19.39 19.39 0 0 0-10.67 4.111c0.591 0.5 1.258 1.168 2.663 2.553 2.63 2.591 9.375 5.275 9.39 5.59z" style="fill:url(#f)"/>
|
||||
<path d="m24.96 24.36c0.314 0.2 0.573 0.374 0.8 0.531a17.97 17.97 0 0 1-0.109-9.473 28.7 28.7 0 0 0-9.329 7.21c0.189-5e-3 5.811-0.106 8.638 1.732z" style="fill:url(#g)"/>
|
||||
<path d="m0.354 42.16c2.991 17.67 19.01 31.17 37.19 31.68 16.83 0.476 27.58-9.294 32.02-18.82a35.1 35.1 0 0 0 1.206-26.16v-0.037c0-0.029-6e-3 -0.046 0-0.037l7e-3 0.065c1.375 8.977-3.191 17.67-10.33 23.56l-0.022 0.05c-13.91 11.33-27.22 6.834-29.91 5q-0.282-0.135-0.564-0.281c-8.109-3.876-11.46-11.26-10.74-17.6a9.953 9.953 0 0 1-9.181-5.775 14.62 14.62 0 0 1 14.25-0.572 19.3 19.3 0 0 0 14.55 0.572c-0.015-0.315-6.76-3-9.39-5.59-1.405-1.385-2.072-2.052-2.663-2.553a11.59 11.59 0 0 0-1-0.758c-0.23-0.157-0.489-0.327-0.8-0.531-2.827-1.838-8.449-1.737-8.635-1.732h-0.018c-1.536-1.947-1.428-8.367-1.34-9.708a6.928 6.928 0 0 0-1.294 0.687 28.22 28.22 0 0 0-3.788 3.245 33.84 33.84 0 0 0-3.638 4.337v6e-3 -7e-3a32.73 32.73 0 0 0-5.2 11.74c-0.019 0.079-1.396 6.099-0.717 9.221z" style="fill:url(#h)"/>
|
||||
<path d="m52.42 26.86a21.1 21.1 0 0 1 3.6 4.7c0.213 0.161 0.412 0.321 0.581 0.476 8.787 8.1 4.183 19.55 3.84 20.36 7.138-5.881 11.7-14.58 10.33-23.56-4.384-10.93-11.82-15.34-17.88-24.93-0.307-0.485-0.614-0.971-0.913-1.484-0.171-0.293-0.308-0.557-0.427-0.8a7.053 7.053 0 0 1-0.578-1.535 0.1 0.1 0 0 0-0.088-0.1 0.138 0.138 0 0 0-0.073 0c-5e-3 0-0.013 9e-3 -0.019 0.011s-0.019 0.011-0.028 0.015c-1.499 0.711-10.39 14.79 1.66 26.83z" style="fill:url(#i)"/>
|
||||
<path d="m56.6 32.04c-0.169-0.155-0.368-0.315-0.581-0.476-0.079-0.059-0.157-0.118-0.241-0.177a13.38 13.38 0 0 0-9.345-2.269c13.94 6.97 10.2 30.97-9.119 30.07a17.24 17.24 0 0 1-5.043-0.973q-0.569-0.213-1.138-0.466c-0.219-0.1-0.438-0.2-0.654-0.312l0.027 0.017c2.694 1.839 16 6.332 29.91-5l0.022-0.05c0.347-0.81 4.951-12.26-3.84-20.36z" style="fill:url(#j)"/>
|
||||
<path d="m21.35 44.74s1.789-6.667 12.81-6.667c1.191 0 4.6-3.325 4.661-4.289a19.3 19.3 0 0 1-14.55-0.572 14.62 14.62 0 0 0-14.25 0.572 9.953 9.953 0 0 0 9.181 5.775c-0.718 6.337 2.632 13.72 10.74 17.6 0.181 0.087 0.351 0.181 0.537 0.264-4.733-2.445-8.641-7.069-9.129-12.68z" style="fill:url(#k)"/>
|
||||
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76l7e-3 0.065c-4.382-10.92-11.81-15.33-17.88-24.92-0.307-0.485-0.614-0.971-0.913-1.484-0.171-0.293-0.308-0.557-0.427-0.8a7.053 7.053 0 0 1-0.578-1.535 0.1 0.1 0 0 0-0.088-0.1 0.138 0.138 0 0 0-0.073 0c-5e-3 0-0.013 9e-3 -0.019 0.011s-0.019 0.011-0.028 0.015l0.015-0.026c-9.735 5.7-13.04 16.25-13.34 21.53 0.452-0.031 0.9-0.069 1.362-0.069a19.56 19.56 0 0 1 16.98 9.917 13.38 13.38 0 0 0-9.345-2.269c13.94 6.97 10.2 30.97-9.119 30.07a17.24 17.24 0 0 1-5.043-0.973q-0.569-0.213-1.138-0.466c-0.219-0.1-0.438-0.2-0.654-0.312l0.027 0.017q-0.282-0.135-0.564-0.281c0.181 0.087 0.351 0.181 0.537 0.264-4.733-2.446-8.641-7.07-9.129-12.68 0 0 1.789-6.667 12.81-6.667 1.191 0 4.6-3.325 4.661-4.289-0.015-0.315-6.76-3-9.39-5.59-1.405-1.385-2.072-2.052-2.663-2.553a11.59 11.59 0 0 0-1-0.758 17.97 17.97 0 0 1-0.109-9.473 28.7 28.7 0 0 0-9.329 7.21h-0.018c-1.536-1.947-1.428-8.367-1.34-9.708a6.928 6.928 0 0 0-1.294 0.687 28.22 28.22 0 0 0-3.788 3.245 33.84 33.84 0 0 0-3.623 4.347v6e-3 -7e-3a32.73 32.73 0 0 0-5.2 11.74l-0.052 0.256c-0.073 0.341-0.4 2.073-0.447 2.445v0a45.09 45.09 0 0 0-0.572 5.403v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47zm-3.845 1.991 7e-3 0.041z" style="fill:url(#l)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
8
docs/_media/icons/google-chrome.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#4285F4" d="M11.9733005,16.4144516 C9.59429509,16.4144516 7.65884342,14.4788121 7.65884342,12.0999945 C7.65884342,9.72075429 9.59429509,7.78530262 11.9733005,7.78530262 C14.3523059,7.78530262 16.2879454,9.72075429 16.2879454,12.0999945 C16.2879924,14.4788121 14.3523529,16.4144516 11.9733005,16.4144516 L11.9733005,16.4144516 Z"/>
|
||||
<path fill="#4AAE48" d="M13.7910066,17.1810894 C13.1872494,17.4007528 12.5549364,17.5116882 11.9068426,17.5116882 C10.6352186,17.5116882 9.39370022,17.0652225 8.41129728,16.2548117 C7.61431896,15.5972306 7.02033082,14.7318218 6.69297277,13.7500294 L6.691235,13.7442055 L1.93641793,5.50854377 C0.228238232,8.1357683 -0.377867323,11.2684017 0.230163868,14.3493248 C0.849467072,17.4870306 2.65331799,20.1955546 5.30970885,21.9764857 C6.69353637,22.9040785 8.22657716,23.5227711 9.86735961,23.8160783 L13.7910066,17.1810894 L13.7910066,17.1810894 Z"/>
|
||||
<path fill="#EA3939" d="M22.7599128,6.70666487 C19.7903479,0.731840215 12.5393434,-1.70437065 6.56465968,1.26519432 C5.01832731,2.03375777 3.65958002,3.12347966 2.57441389,4.45616042 L6.59105498,11.413435 C6.85172029,9.39264477 8.25048322,7.60626547 10.3219975,6.95469613 C10.8217704,6.79763941 11.341739,6.71403864 11.865371,6.70666487 L22.7599128,6.70666487 Z"/>
|
||||
<path fill="#FED14B" d="M11.9264747,24 C14.936431,24 17.8171819,22.8712018 20.0368292,20.8218558 C22.2681243,18.7619893 23.6231612,15.9588274 23.8523118,12.9290041 C23.983349,11.1937716 23.7261592,9.41711443 23.1082181,7.78530262 L15.2520944,7.78530262 C16.5738788,8.83162726 17.3494403,10.4306093 17.3416908,12.1250278 C17.3360548,13.3449884 16.9177692,14.5439079 16.1598672,15.5064909 L11.1518525,23.9751076 C11.4097938,23.9916399 11.6696606,24 11.926052,24 L11.9264747,24 L11.9264747,24 Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
44
docs/_media/icons/microsoft-edge.svg
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="linearGradient-1" gradientUnits="userSpaceOnUse" x1="63.3343" y1="757.83" x2="241.6165" y2="757.83" gradientTransform="matrix(1 0 0 1 -4.63 -580.8098)">
|
||||
<stop offset="0" style="stop-color:#0C59A4"/>
|
||||
<stop offset="1" style="stop-color:#114A8B"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="radialGradient-1" cx="161.83" cy="788.4008" r="95.38" gradientTransform="matrix(0.9999 0 0 0.9498 -4.6217 -570.3868)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.72" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="0.95" style="stop-color:#000000;stop-opacity:0.53"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="linearGradient-2" gradientUnits="userSpaceOnUse" x1="157.4013" y1="680.5561" x2="46.0276" y2="801.8683" gradientTransform="matrix(1 0 0 1 -4.63 -580.8098)">
|
||||
<stop offset="0" style="stop-color:#1B9DE2"/>
|
||||
<stop offset="0.16" style="stop-color:#1595DF"/>
|
||||
<stop offset="0.67" style="stop-color:#0680D7"/>
|
||||
<stop offset="1" style="stop-color:#0078D4"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="radialGradient-2" cx="-773.6357" cy="746.7146" r="143.24" gradientTransform="matrix(0.15 -0.9898 0.8 0.12 -410.7182 -656.3412)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.76" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="0.95" style="stop-color:#000000;stop-opacity:0.5"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="radialGradient-3" cx="230.5926" cy="-106.0381" r="202.4299" gradientTransform="matrix(-3.999750e-02 0.9998 -2.1299 -7.998414e-02 -190.7749 -191.6354)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#35C1F1"/>
|
||||
<stop offset="0.11" style="stop-color:#34C1ED"/>
|
||||
<stop offset="0.23" style="stop-color:#2FC2DF"/>
|
||||
<stop offset="0.31" style="stop-color:#2BC3D2"/>
|
||||
<stop offset="0.67" style="stop-color:#36C752"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="radialGradient-4" cx="536.3567" cy="-117.7029" r="97.34" gradientTransform="matrix(0.28 0.9598 -0.78 0.23 -1.9279 -410.3179)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#66EB6E"/>
|
||||
<stop offset="1" style="stop-color:#66EB6E;stop-opacity:0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<path fill="url(#linearGradient-1)" d="M231,190.5c-3.4,1.8-6.9,3.4-10.5,4.7c-11.5,4.3-23.6,6.5-35.9,6.5c-47.3,0-88.5-32.5-88.5-74.3 c0.1-11.4,6.4-21.9,16.4-27.3c-42.8,1.8-53.8,46.4-53.8,72.5c0,73.9,68.1,81.4,82.8,81.4c7.9,0,19.8-2.3,27-4.6l1.3-0.4 c27.6-9.5,51-28.1,66.6-52.8c1.2-1.9,0.6-4.3-1.2-5.5C233.9,189.9,232.3,189.8,231,190.5z"/>
|
||||
<path opacity="0.35" fill="url(#radialGradient-1)" enable-background="new " d="M231,190.5c-3.4,1.8-6.9,3.4-10.5,4.7 c-11.5,4.3-23.6,6.5-35.9,6.5c-47.3,0-88.5-32.5-88.5-74.3c0.1-11.4,6.4-21.9,16.4-27.3c-42.8,1.8-53.8,46.4-53.8,72.5 c0,73.9,68.1,81.4,82.8,81.4c7.9,0,19.8-2.3,27-4.6l1.3-0.4c27.6-9.5,51-28.1,66.6-52.8c1.2-1.9,0.6-4.3-1.2-5.5 C233.9,189.9,232.3,189.8,231,190.5z"/>
|
||||
<path fill="url(#linearGradient-2)" d="M105.7,241.4c-8.9-5.5-16.6-12.8-22.7-21.3c-26.3-36-18.4-86.5,17.6-112.8c3.8-2.7,7.7-5.2,11.9-7.2 c3.1-1.5,8.4-4.1,15.5-4c10.1,0.1,19.6,4.9,25.7,13c4,5.4,6.3,11.9,6.4,18.7c0-0.2,24.5-79.6-80-79.6c-43.9,0-80,41.7-80,78.2 c-0.2,19.3,4,38.5,12.1,56c27.6,58.8,94.8,87.6,156.4,67.1C147.5,256.1,124.5,253.2,105.7,241.4L105.7,241.4z"/>
|
||||
<path opacity="0.41" fill="url(#radialGradient-2)" enable-background="new " d="M105.7,241.4c-8.9-5.5-16.6-12.8-22.7-21.3 c-26.3-36-18.4-86.5,17.6-112.8c3.8-2.7,7.7-5.2,11.9-7.2c3.1-1.5,8.4-4.1,15.5-4c10.1,0.1,19.6,4.9,25.7,13 c4,5.4,6.3,11.9,6.4,18.7c0-0.2,24.5-79.6-80-79.6c-43.9,0-80,41.7-80,78.2c-0.2,19.3,4,38.5,12.1,56 c27.6,58.8,94.8,87.6,156.4,67.1C147.5,256.1,124.5,253.2,105.7,241.4L105.7,241.4z"/>
|
||||
<path fill="url(#radialGradient-3)" d="M152.3,148.9c-0.8,1-3.3,2.5-3.3,5.7c0,2.6,1.7,5.1,4.7,7.2c14.4,10,41.5,8.7,41.6,8.7 c10.7,0,21.1-2.9,30.3-8.3c18.8-11,30.4-31.1,30.4-52.9c0.3-22.4-8-37.3-11.3-43.9C223.5,23.9,177.7,0,128,0C58,0,1,56.2,0,126.2 c0.5-36.5,36.8-66,80-66c3.5,0,23.5,0.3,42,10.1c16.3,8.6,24.9,18.9,30.8,29.2c6.2,10.7,7.3,24.1,7.3,29.5 C160.1,134.3,157.4,142.3,152.3,148.9z"/>
|
||||
<path fill="url(#radialGradient-4)" d="M152.3,148.9c-0.8,1-3.3,2.5-3.3,5.7c0,2.6,1.7,5.1,4.7,7.2c14.4,10,41.5,8.7,41.6,8.7 c10.7,0,21.1-2.9,30.3-8.3c18.8-11,30.4-31.1,30.4-52.9c0.3-22.4-8-37.3-11.3-43.9C223.5,23.9,177.7,0,128,0C58,0,1,56.2,0,126.2 c0.5-36.5,36.8-66,80-66c3.5,0,23.5,0.3,42,10.1c16.3,8.6,24.9,18.9,30.8,29.2c6.2,10.7,7.3,24.1,7.3,29.5 C160.1,134.3,157.4,142.3,152.3,148.9z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.7 KiB |
19
docs/_media/icons/opera.svg
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="100px" height="100px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<g transform="scale(0.521)">
|
||||
<g transform="scale(1.8) translate(3,3)">
|
||||
<defs>
|
||||
<radialGradient id="RG1" cx="50%" cy="50%" fx="50%" fy="50%" r="50%">
|
||||
<stop style="stop-color:rgb(255,29,48);stop-opacity:0.75;" offset="0%"/>
|
||||
<stop style="stop-color:rgb(180,4,15);stop-opacity:1;" offset="100%"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="LG1" x1="20%" y1="80%" x2="20%" y2="0%">
|
||||
<stop style="stop-color:rgb(253,76,86);stop-opacity:0.75;" offset="0%"/>
|
||||
<stop style="stop-color:rgb(158,4,4);stop-opacity:1;" offset="100%"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path style="fill:url(#RG1)" d="m 81,11 a 50,50 1 1 0 0,78 a 36.9,45 0 1 1 0,-78"/>
|
||||
<path style="fill:url(#LG1)" d="m 36,19 a 36.9,45 1 0 1 45,-8 a 50,50 1 0 1 0,78 a 36.9,45 1 0 1 -45,-8 a 26.5,35 1 1 0 0,-62"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
BIN
docs/_media/icons/remmina.png
Normal file
After Width: | Height: | Size: 11 KiB |
31
docs/_media/icons/tor-browser.svg
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512px" height="512px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="100%" x2="50%" y2="0%" id="linearGradient-1">
|
||||
<stop stop-color="#420C5D" offset="0%"/>
|
||||
<stop stop-color="#951AD1" offset="100%"/>
|
||||
</linearGradient>
|
||||
<path d="M25,29 C152.577777,29 256,131.974508 256,259 C256,386.025492 152.577777,489 25,489 L25,29 Z" id="path-2"/>
|
||||
<filter x="-18.2%" y="-7.4%" width="129.4%" height="114.8%" filterUnits="objectBoundingBox" id="filter-3">
|
||||
<feOffset dx="-8" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"/>
|
||||
<feGaussianBlur stdDeviation="10" in="shadowOffsetOuter1" result="shadowBlurOuter1"/>
|
||||
<feColorMatrix values="0 0 0 0 0.250980392 0 0 0 0 0.250980392 0 0 0 0 0.250980392 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="tor-browser-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="icon_512x512">
|
||||
<g id="Group">
|
||||
<g id="tb_icon/Stable">
|
||||
<g id="Stable">
|
||||
<circle id="background" fill="#F2E4FF" fill-rule="nonzero" cx="256" cy="256" r="246"/>
|
||||
<path d="M256.525143,465.439707 L256.525143,434.406609 C354.826191,434.122748 434.420802,354.364917 434.420802,255.992903 C434.420802,157.627987 354.826191,77.8701558 256.525143,77.5862948 L256.525143,46.5531962 C371.964296,46.8441537 465.446804,140.489882 465.446804,255.992903 C465.446804,371.503022 371.964296,465.155846 256.525143,465.439707 Z M256.525143,356.820314 C311.970283,356.529356 356.8487,311.516106 356.8487,255.992903 C356.8487,200.476798 311.970283,155.463547 256.525143,155.17259 L256.525143,124.146588 C329.115485,124.430449 387.881799,183.338693 387.881799,255.992903 C387.881799,328.654211 329.115485,387.562455 256.525143,387.846316 L256.525143,356.820314 Z M256.525143,201.718689 C286.266674,202.00255 310.3026,226.180407 310.3026,255.992903 C310.3026,285.812497 286.266674,309.990353 256.525143,310.274214 L256.525143,201.718689 Z M0,255.992903 C0,397.384044 114.60886,512 256,512 C397.384044,512 512,397.384044 512,255.992903 C512,114.60886 397.384044,0 256,0 C114.60886,0 0,114.60886 0,255.992903 Z" id="center" fill="url(#linearGradient-1)"/>
|
||||
<g id="half" transform="translate(140.500000, 259.000000) scale(-1, 1) translate(-140.500000, -259.000000) ">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"/>
|
||||
<use fill="url(#linearGradient-1)" fill-rule="evenodd" xlink:href="#path-2"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
1
docs/_media/icons/vivaldi.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="1900px" height="1900px" viewBox="0 0 1900 1900" xmlns="http://www.w3.org/2000/svg"><path fill="#ef3939" d="M944 1830c386 0 600 0 740-140s140-354 140-740 0-600-140-740S1330 70 944 70s-600 0-740 140S64 564 64 950s0 600 140 740 354 140 740 140z"/><linearGradient id="a" x1="61.24" x2="145.33" y1="37.94" y2="183.58" gradientUnits="userSpaceOnUse"><stop offset="0" stop-opacity=".2"/><stop offset=".79" stop-opacity=".05"/></linearGradient><path fill="url(#a)" d="M151.6 62.4A66 66 0 0030.5 78a65.57 65.57 0 006.8 50.4c.1.2.2.4.4.6l31 53.8 25.5.2c17.1 0 30.9 0 42.2-1.2 14-1.5 24.1-5 31.9-12.8 11.3-11.3 13.5-27.6 13.9-53.8l-30.6-52.8z" transform="scale(10)"/><path fill="#fff" d="M1407 484a657.9 657.9 0 00-932 0 660.9 660.9 0 000 933 657.9 657.9 0 00932 0 660.9 660.9 0 000-933zm-39 304l-326 567c-20 35-49 56-90 59-45 3-80-16-103-55L519 786c-42-73 5-162 89-166 44-2 78 18 101 57 31 52 61 105 91 158l66 114c33 55 80 85 144 89 90 5 174-60 185-156 1-7 1-14 2-18 0-31-6-57-19-82-34-68 2-143 75-160 60-13 121 31 129 91 4 27-1 52-14 75z"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
107
docs/_media/icons/vlc.svg
Normal file
After Width: | Height: | Size: 237 KiB |
95
docs/_media/icons/xfce.svg
Normal file
@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="760" height="760" id="svg586">
|
||||
<title id="title3704">XFCE 4 Logo</title>
|
||||
<metadata id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title>XFCE 4 Logo</dc:title>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Savvas Radevic</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:source>http://www.xfce.org/about/artwork</dc:source>
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/"/>
|
||||
<dc:description>XFCE logo
|
||||
* Based on xfce_logo.svg from http://www.xfce.org/about/artwork
|
||||
* Optimized colours
|
||||
* Added "X" and "XFCE" text.
|
||||
</dc:description>
|
||||
</cc:Work>
|
||||
<cc:License rdf:about="http://creativecommons.org/licenses/by-sa/3.0/">
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
|
||||
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
|
||||
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
|
||||
<cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs id="defs588">
|
||||
<linearGradient id="linearGradient4380">
|
||||
<stop id="stop4382" style="stop-color:#000000" offset="0"/>
|
||||
<stop id="stop4384" style="stop-color:#000000;stop-opacity:0" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3694">
|
||||
<stop id="stop3702" style="stop-color:#b7b7b7" offset="0"/>
|
||||
<stop id="stop3698" style="stop-color:#000000" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3743">
|
||||
<stop id="stop3745" style="stop-color:#ffffff" offset="0"/>
|
||||
<stop id="stop3751" style="stop-color:#7fd4ee" offset="0.5"/>
|
||||
<stop id="stop3747" style="stop-color:#00aade" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3652">
|
||||
<stop id="stop3654" style="stop-color:#ffffff" offset="0"/>
|
||||
<stop id="stop3662" style="stop-color:#d5e2ec" offset="0.49903482"/>
|
||||
<stop id="stop3656" style="stop-color:#d5e2ec" offset="1"/>
|
||||
</linearGradient>
|
||||
<filter color-interpolation-filters="sRGB" id="filter3729">
|
||||
<feGaussianBlur stdDeviation="6.4154088" id="feGaussianBlur3731"/>
|
||||
</filter>
|
||||
<linearGradient x1="965.02625" y1="17.489901" x2="1557.8665" y2="663.80927" id="linearGradient3737" xlink:href="#linearGradient3652" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1581.6355,147.78233)"/>
|
||||
<linearGradient x1="33.526711" y1="441.98093" x2="159.24117" y2="581.41302" id="linearGradient3966" xlink:href="#linearGradient3743" gradientUnits="userSpaceOnUse"/>
|
||||
<linearGradient x1="245.51741" y1="426.95151" x2="305.39166" y2="531.48969" id="linearGradient3968" xlink:href="#linearGradient3743" gradientUnits="userSpaceOnUse" gradientTransform="translate(-0.38239847,2.6767893)"/>
|
||||
<linearGradient x1="601.53467" y1="434.94836" x2="668.81775" y2="543.35834" id="linearGradient3970" xlink:href="#linearGradient3743" gradientUnits="userSpaceOnUse"/>
|
||||
<linearGradient x1="440.52084" y1="455.93307" x2="486.95523" y2="535.30469" id="linearGradient3972" xlink:href="#linearGradient3743" gradientUnits="userSpaceOnUse" gradientTransform="translate(-0.50986462,6.1183754)"/>
|
||||
<linearGradient x1="118.25153" y1="254.24648" x2="291.87143" y2="254.24648" id="linearGradient4386" xlink:href="#linearGradient4380" gradientUnits="userSpaceOnUse"/>
|
||||
<filter color-interpolation-filters="sRGB" id="filter4631">
|
||||
<feGaussianBlur id="feGaussianBlur4633" stdDeviation="2.3336003"/>
|
||||
</filter>
|
||||
<linearGradient x1="174.28104" y1="164.16707" x2="236.40213" y2="253.70575" id="linearGradient4162" xlink:href="#linearGradient3694" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.7524926,0,0,3.7524926,-424.92228,-686.03042)"/>
|
||||
<filter color-interpolation-filters="sRGB" id="filter4223">
|
||||
<feGaussianBlur id="feGaussianBlur4225" stdDeviation="4.6672006"/>
|
||||
</filter>
|
||||
<filter color-interpolation-filters="sRGB" id="filter4266">
|
||||
<feGaussianBlur id="feGaussianBlur4268" stdDeviation="1.308628"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(754.35152,-70.104999)" id="g3733">
|
||||
<path d="M -688.36669,231.76247 -533.89627,88.530764 -359.32377,303.30745 -170.75037,88.081185 -25.665766,221.31553 -234.39997,435.40777 -20.542666,663.49077 -175.64877,807.89088 -372.21127,579.61811 -569.06737,810.83624 -709.17754,672.17796 -494.29357,442.77473 -688.36669,231.76247 z" id="path2870-8" style="filter:url(#filter3729)"/>
|
||||
<path d="M -692.09302,232.01783 -537.6226,88.786116 -363.0501,303.5628 -174.4767,88.336537 -29.392112,221.57089 -238.1263,435.66312 -24.269012,663.74612 -179.3751,808.14623 -375.9376,579.87346 -572.7937,811.09159 -712.90387,672.43331 -498.0199,443.03008 -692.09302,232.01783 z" id="path2870" style="fill:url(#linearGradient3737)"/>
|
||||
</g>
|
||||
<g transform="translate(20.600025,32.751608)" id="g3915">
|
||||
<g transform="translate(6,4.5)" id="g3803-6" style="opacity:0.8;filter:url(#filter4223)">
|
||||
<path d="m 58.345585,417.63579 56.432265,69.38376 56.45069,-66.66449 40.49963,40.30749 -61.78078,65.91662 65.45855,68.01565 -42.74601,42.50269 L 108.5111,564.35998 48.268494,638.64971 7.1899623,597.32826 74.708237,531.54185 13.029005,462.45552 z" id="path3912" style="stroke:#000000;stroke-width:3"/>
|
||||
<path d="m 245.32235,432.76045 c 0,0 117.20479,-1.21951 117.20479,-0.74369 0,0.47308 -0.46199,46.74243 -0.46199,46.74243 l -56.15161,2.88037 -0.17056,36.17 53.92068,0.57225 0.17335,44.40991 -53.06171,1.1226 -0.0416,75.28312 -62.11058,-0.88382 c 0,0 -0.24879,-205.55317 0.69921,-205.55317 z" id="path3914" style="stroke:#000000;stroke-width:3"/>
|
||||
<path d="m 542.33906,431.37943 c 3.7919,18.02364 7.581,36.04727 11.373,54.07091 -36.2734,12.22845 -88.71272,15.00605 -91.24882,68.09506 6.6716,52.01081 70.35438,34.37103 98.68398,36.76942 0.4726,17.23426 0.43804,33.70097 0.91074,50.93523 C 435.29236,657.93973 400.36214,596.11929 398.22454,573.7307 385.72484,472.89955 486.80926,443.52538 542.33906,431.37943 z" id="path3916" style="stroke:#000000;stroke-width:3"/>
|
||||
<path d="m 597.26819,428.94061 116.39026,-0.42207 -1.34547,46.26995 -56.4993,0.0539 0.79926,34.59126 48.82809,-0.68549 -0.56651,45.70916 -48.37488,0.80727 0.4754,32.7275 57.10463,0.0964 0.11331,50.46219 -118.30011,-3.34717 z" id="path3918" style="stroke:#000000;stroke-width:3"/>
|
||||
</g>
|
||||
<g id="g4621">
|
||||
<path d="m 58.345585,417.63579 56.432265,69.38376 56.45069,-66.66449 40.49963,40.30749 -61.78078,65.91662 65.45855,68.01565 -42.74601,42.50269 L 108.5111,564.35998 48.268494,638.64971 7.1899623,597.32826 74.708237,531.54185 13.029005,462.45552 z" id="path4623" style="fill:url(#linearGradient3966);stroke:#00aade;stroke-width:3"/>
|
||||
<path d="m 245.32235,432.76045 c 0,0 117.20479,-1.21951 117.20479,-0.74369 0,0.47308 -0.46199,46.74243 -0.46199,46.74243 l -56.15161,2.88037 -0.17056,36.17 53.92068,0.57225 0.17335,44.40991 -53.06171,1.1226 -0.0416,75.28312 -62.11058,-0.88382 c 0,0 -0.24879,-205.55317 0.69921,-205.55317 z" id="path4625" style="fill:url(#linearGradient3968);stroke:#00aade;stroke-width:3"/>
|
||||
<path d="m 542.33906,431.37943 c 3.7919,18.02364 7.581,36.04727 11.373,54.07091 -36.2734,12.22845 -88.71272,15.00605 -91.24882,68.09506 6.6716,52.01081 70.35438,34.37103 98.68398,36.76942 0.4726,17.23426 0.43804,33.70097 0.91074,50.93523 C 435.29236,657.93973 400.36214,596.11929 398.22454,573.7307 385.72484,472.89955 486.80926,443.52538 542.33906,431.37943 z" id="path4627" style="fill:url(#linearGradient3972);stroke:#00aade;stroke-width:3"/>
|
||||
<path d="m 597.26819,428.94061 116.39026,-0.42207 -1.34547,46.26995 -56.4993,0.0539 0.79926,34.59126 48.82809,-0.68549 -0.56651,45.70916 -48.37488,0.80727 0.4754,32.7275 57.10463,0.0964 0.11331,50.46219 -118.30011,-3.34717 z" id="path4629" style="fill:url(#linearGradient3970);stroke:#00aade;stroke-width:3"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(3.7524926,0,0,3.7524926,-424.92228,-686.03042)" id="g23" style="fill:url(#linearGradient4386);stroke:#000000;stroke-width:0.26814505">
|
||||
<use transform="translate(1.8654516,1.1992028)" id="use4246" style="opacity:0.8;filter:url(#filter4266)" x="0" y="0" width="760" height="760" xlink:href="#path14"/>
|
||||
<path d="m 118.94557,209.56947 c -0.915,0.501 7.2975,5.88275 22.6875,12.21875 15.391,6.336 36.667,22.37675 35,24.96875 -4.999,7.041 -7.94125,15.48225 -5.90625,29.90625 0.367,2.539 -7.54275,6.95025 -6.96875,10.78125 0.48,2.915 8.838,-2.896 9.75,-1.75 0.558,2.057 -4.4665,12.47475 -2.4375,13.21875 1.484,0.441 14.52325,-12.4015 15.28125,-12.1875 21.514,6.504 49.5205,1.61225 50.0625,1.40625 -0.051,-0.113 20.12,9.49175 21.875,8.34375 4.427,-4.794 -8.387,-11.551 -6.25,-14.25 0.937,-1.413 15.62075,5.30275 16.34375,3.46875 1.052,-3.175 -6.817,-9.8375 -8,-11.8125 -1.029,-1.821 -1.23275,-4.75375 1.40625,-6.59375 2.64,-1.84 29.632,-12.16625 28.5,-20.15625 -1.957,-14.518 -34.26025,-15.4655 -34.78125,-16.6875 -0.245,-0.574 1.44825,-20.76025 -7.96875,-17.03125 -5.798,2.158 -1.09375,15.0245 -0.71875,16.6875 0.346,1.531 -2.574,1.3595 -3.125,0.6875 -1.23,-1.502 -5.2725,-14.92525 -10.0625,-12.53125 -6.718,3.704 -0.41,15.28125 -1.75,15.65625 -2.489,1.391 -2.34875,4.7945 -4.34375,7.5625 -3.29,4.715 -27.917,-2.2055 -35.875,-2.6875 -5.826,-0.344 -11.38025,4.4205 -11.90625,3.8125 -7.094,-7.104 -21.76475,-19.48475 -37.84375,-23.59375 -9.017,-2.447 -22.43075,-9.5275 -22.96875,-9.4375 z m 164.21875,13 c -0.53716,0.23183 -2.70444,11.39669 -1.65625,11.90625 0.929,0.695 2.5355,-11.257 1.6875,-11.875 -0.0138,-0.016 -0.0139,-0.0387 -0.0313,-0.0313 z m 7.96875,1.53125 c -1.21841,-0.0224 -7.71547,10.3605 -6.21875,11.46875 0.929,0.695 6.7105,-11.0445 6.3125,-11.4375 -0.0278,-0.023 -0.0544,-0.0305 -0.0937,-0.0313 z m -31.5,12.65625 c 1.53924,0 2.78125,1.40374 2.78125,3.15625 0,1.75251 -1.24201,3.1875 -2.78125,3.1875 -1.53924,0 -2.78125,-1.43499 -2.78125,-3.1875 0,-1.75251 1.24201,-3.15625 2.78125,-3.15625 z m -6.25,1.90625 c 1.184,5.111 3.0165,6.7255 8.5625,6.5625 -7.059,6.473 -11.3335,-4.6035 -8.5625,-6.5625 z m 14.9375,13.375 c 0.43934,-0.0356 0.77562,0.0333 0.96875,0.21875 2.781,1.008 -11.93325,6.3115 -12.03125,5.5625 -0.14787,-0.51625 7.98709,-5.53182 11.0625,-5.78125 z m 1.8125,3.375 c 0.15222,-0.0118 0.29388,0.0358 0.40625,0.0937 2.625,2.608 -8.3105,8.3905 -8.1875,8.1875 -0.88125,-0.5325 5.498,-8.1041 7.78125,-8.28125 z" id="path14" style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.26814505"/>
|
||||
<path d="m 21.40625,100.375 c -3.433531,1.88 27.405389,22.06796 85.15625,45.84375 57.75461,23.77579 137.59916,83.99229 131.34375,93.71875 -18.75871,26.4213 -29.82382,58.0928 -22.1875,112.21875 1.37716,9.52758 -28.27893,26.0617 -26.125,40.4375 1.62157,9.84768 27.16679,-6.83583 34.75,-7.125 C 211.59347,288.40086 380.71993,233.48854 524.375,211.125 c 0.42249,-0.61852 0.9417,-1.14571 1.53125,-1.5625 0.10346,0.44662 0.20625,0.84878 0.3125,1.28125 4.64119,-0.71462 9.26483,-1.41562 13.84375,-2.0625 1.73622,-3.79693 5.24284,-6.375 9.28125,-6.375 3.18263,0 6.02524,1.58871 7.9375,4.125 23.87521,-2.99523 46.50573,-5.06548 66.84375,-6.21875 -37.95486,-16.11541 -88.98683,-18.63101 -90.25,-21.59375 -0.91936,-2.15393 5.43097,-77.89929 -29.90625,-63.90625 -21.75695,8.09788 -4.09468,56.35335 -2.6875,62.59375 1.29836,5.74507 -9.68238,5.11543 -11.75,2.59375 -4.61557,-5.63624 -19.77556,-56.01472 -37.75,-47.03125 -25.20925,13.89923 -1.53416,57.34282 -6.5625,58.75 -9.33995,5.21972 -8.82628,17.9881 -16.3125,28.375 -12.3457,17.693 -104.76266,-8.2538 -134.625,-10.0625 -21.86202,-1.29086 -42.68244,16.56277 -44.65625,14.28125 -26.62018,-26.65771 -81.66367,-73.11226 -142,-88.53125 C 73.788774,126.5989 23.425091,100.03728 21.40625,100.375 z" transform="matrix(0.26648953,0,0,0.26648953,113.23734,182.81993)" id="path4083" style="fill:url(#linearGradient4162)"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |