153 Commits

Author SHA1 Message Date
c873d4d344 screenshare POC. 2023-01-29 21:29:16 +01:00
72c0070a3a update changelog. 2023-01-29 20:31:00 +01:00
009ceddbd3 gst: move sample channel to AttachAppsink. 2023-01-29 20:31:00 +01:00
fdf17cfe77 channel should not close on destroy 2023-01-29 20:31:00 +01:00
628c6a1b77 remove wait timer from goroutine 2023-01-29 20:31:00 +01:00
79a1c41938 ensure fps is not 0. 2023-01-29 20:31:00 +01:00
64b79f4579 update. 2023-01-29 20:31:00 +01:00
8d0468ea62 codec is video & audio. 2023-01-29 20:31:00 +01:00
89737dd4ce codec is video & audio. 2023-01-29 20:31:00 +01:00
2649594c2e strongly typed session events channel. 2023-01-29 20:31:00 +01:00
f3080713ce join GetScreenSizeChangeChannel. 2023-01-29 20:31:00 +01:00
6e62b796fc remove unused channels. 2023-01-29 20:31:00 +01:00
4094639ea9 adaptive fps moved to pipeline creation. 2023-01-29 20:31:00 +01:00
c45a315d9b removed adaptive framerate tag and react to closed channels 2023-01-29 20:31:00 +01:00
ee13e40d4c go fmt. 2023-01-29 20:31:00 +01:00
dfe8b8b57d npm lint. 2023-01-29 20:31:00 +01:00
12623866b3 disableHostCheck -> allowedHosts. 2023-01-29 20:31:00 +01:00
161d121e59 channel direct from the pipeline 2023-01-29 20:31:00 +01:00
5690a849e2 remove go-events 2023-01-29 20:31:00 +01:00
cfc6bd417f Update README.md 2023-01-15 22:21:44 +01:00
32472a70bc Fix docker build (#237)
* include package-lock.

* extract intel gpu support from dockerfile.

* update arm support.

* new workflows.

* build intel images.

* add to docs.
2023-01-15 20:38:29 +01:00
cd9ac70cb9 update XkbAddKeyKeysym. 2023-01-15 16:23:48 +01:00
a02f47f140 update xorg bindings. 2023-01-14 21:15:52 +01:00
ccc1df936d fix stereo for chormium browsers. 2023-01-12 20:53:36 +01:00
1509521129 Merge pull request #232 from Dishwasha/master
Use Keyboard API to lock screen for supported browsers
2023-01-07 11:58:36 +01:00
10e2fbd499 Use Keyboard API to lock screen for supported browsers
When using fullscreen mode, hitting the escape key will exit out of
fullscreen mode.  To stay in fullscreen mode but pass the escape
key through, we can use the Keyboard API to require the escape key
be held down https://wicg.github.io/keyboard-lock/#escape-key.

Tested in Chrome which supports Keyboard API and Firefox which does
not https://caniuse.com/?search=navigator.keyboard with no errors
in either case.
2023-01-06 20:10:34 -05:00
1c228a7daa update troubleshooting docs. 2023-01-04 23:16:17 +01:00
e63e9d5b2f update readme. 2022-12-22 21:40:31 +01:00
4789b7704f fix tor browser folder name. 2022-12-13 23:53:50 +01:00
bd73dfae9d Fix WebRTC mux issues. 2022-12-13 23:40:22 +01:00
c8c39b0f83 version 2.7. 2022-11-23 00:04:00 +01:00
61ea7b2940 allow downloads folder in chrome policies. 2022-11-23 00:03:51 +01:00
4d4e6eaeb7 update readme. 2022-11-22 23:54:08 +01:00
2debd4c3f3 upgrade intro gif. 2022-11-22 23:45:15 +01:00
c7885e3a90 update docs. 2022-11-20 14:53:46 +01:00
8c54585f2d add persistent data to docs #223. 2022-11-20 14:50:13 +01:00
e067894e02 opera fetch latest. 2022-11-20 14:41:36 +01:00
407a85fffb add role browser to openbox. 2022-11-20 14:40:35 +01:00
e0366cf1ef do not hide file chooser dialogs. 2022-11-20 14:12:08 +01:00
04a0ce17de fix file tranfser access control. 2022-11-19 23:05:34 +01:00
db87229f16 Merge pull request #221 from prophetofxenu/master
In app file transfer
2022-11-19 22:29:16 +01:00
3df319e071 line height. 2022-11-19 20:55:50 +01:00
ad5abb6054 css fix long file names. 2022-11-19 20:53:27 +01:00
ac822a2531 update docs. 2022-11-19 20:45:38 +01:00
e25e9c2b24 lint. 2022-11-19 20:33:08 +01:00
1666693c25 add cors. 2022-11-19 20:33:03 +01:00
d17a7e8d82 move filetransfer to locks. 2022-11-19 20:26:45 +01:00
cdb9b185f2 filepath clean. 2022-11-19 18:29:21 +01:00
76b44b949c add status failed + error. 2022-11-19 17:35:02 +01:00
950cb118cc remove unused property. 2022-11-19 16:57:00 +01:00
3703d2b73d lint fix. 2022-11-19 16:53:13 +01:00
fac8700192 lint fix. 2022-11-19 16:03:49 +01:00
04cd943eb4 move refresh ws to store. 2022-11-19 15:46:27 +01:00
472a3c3355 Merge branch 'master' of github.com:prophetofxenu/neko 2022-11-19 15:46:11 +01:00
5d41048695 update docs. 2022-11-18 19:41:15 +01:00
42ee7477a0 updated configuration 2022-11-16 22:30:50 -05:00
bbfa0d5834 added translations 2022-11-16 22:03:25 -05:00
4885c2d69e frontend improvements 2022-11-16 20:58:39 -05:00
b65df3e3bf more efficient file upload/download 2022-11-16 20:06:36 -05:00
57e89bb1cc file transfer permission state management 2022-11-15 20:39:06 -05:00
19af921913 added file uploads to frontend 2022-11-13 22:06:24 -05:00
5ba4019e36 abort in progress transfers on cancel (updated axios too) 2022-11-13 11:27:04 -05:00
e3963736ad Merge branch 'm1k1o:master' into master 2022-11-11 20:27:59 -05:00
b9f31cc19c added file downloads to frontend 2022-11-11 20:27:15 -05:00
7c6029aa99 watch file transfer dir for changes 2022-11-04 22:43:18 -04:00
cfc7b15211 manual refresh function 2022-11-03 21:54:05 -04:00
70e84c5840 listing of files on connect 2022-11-02 22:20:32 -04:00
1505abb703 http endpoints for transferring files 2022-10-30 21:06:05 -04:00
758879ef84 fix sweetalert2 version #211. 2022-10-27 20:23:45 +02:00
afd0925cfc upgrade client deps. 2022-10-24 22:29:26 +02:00
7d3a888c79 upgrade go deps. 2022-10-24 21:40:59 +02:00
cfbda6ec97 update changelog. 2022-10-24 19:38:24 +02:00
44e891331e join fonts install. 2022-10-24 19:37:42 +02:00
Bad
8ab2943126 Emoji support
Fixes emojis by adding Google's Noto Emoji.
2022-10-24 20:32:14 +03:00
fa45619dbb added basic UI for file transfer 2022-10-16 20:59:01 -04:00
ec69eb2dcb gramarly disable for textarea 2022-09-27 20:34:14 +02:00
882daddb51 hide resize from textarea. 2022-09-27 20:30:06 +02:00
a9fd6ac114 guac remove text compositon. 2022-09-27 20:28:43 +02:00
fd17a282fa switch overlay to textarea to correctly interpret dead keys. 2022-09-26 18:59:42 +02:00
9f26b27d5d fix capture logging. 2022-09-24 14:21:32 +02:00
d06740aa96 auto broadcast started. 2022-09-21 18:59:38 +02:00
da86a0931c pipelineFn returns string. 2022-09-21 18:58:28 +02:00
b6d86aab30 added opera info to docs 2022-09-20 07:50:54 +02:00
03c74c88b9 added opera image to build 2022-09-20 07:50:54 +02:00
ca6c24dee1 add screenshot function. 2022-09-17 18:37:30 +02:00
057ab2d886 codecs from string not bools. 2022-09-17 18:37:30 +02:00
ccbfe93765 fix register. 2022-09-17 18:37:30 +02:00
fd43f84bd0 refactor capture with broadcast. 2022-09-17 18:37:30 +02:00
72da075972 xorg add events. 2022-09-17 18:37:30 +02:00
2e5d3f5624 update changelog. 2022-09-17 11:01:30 +02:00
9394b361bd xevent on clipboard updated. 2022-09-16 00:01:15 +02:00
e9912ea87f add xevent. 2022-09-15 23:55:30 +02:00
2afc356911 capture use SetScreenSize. 2022-09-13 21:40:50 +02:00
4c1c96b163 xorg refactor. 2022-09-13 21:40:40 +02:00
478984e944 change shutdown order. 2022-09-13 20:35:53 +02:00
e045bd8a1e move locks and bans to state. 2022-09-13 20:35:53 +02:00
06e25df962 websocket handler own submodule. 2022-09-13 20:35:53 +02:00
777f7b4c37 use custom pionlog. 2022-09-13 20:35:53 +02:00
deabba80ca move pipelines from gst to capture. 2022-09-13 20:35:53 +02:00
29f67fad06 move config from types. 2022-09-13 20:35:53 +02:00
c0ca073b2d move gst and broadcast under capture. 2022-09-13 20:35:53 +02:00
de4f6b45e5 split remote to desktop and capture. 2022-09-13 20:35:53 +02:00
0bca8c9d02 remote manager split. 2022-09-13 20:35:53 +02:00
e3e3cf9d22 extract clipboard from xorg. 2022-09-13 20:35:53 +02:00
2be071d215 update readme. 2022-09-11 13:42:54 +02:00
5cd09e6c53 Merge branch 'path-prefix' 2022-09-08 20:00:21 +02:00
537d131883 add to chengelog. 2022-09-08 19:58:10 +02:00
a792e4ea87 fix opera openbox. 2022-09-08 19:56:18 +02:00
7312c17f6a removed unnecessary launch options 2022-09-07 20:19:21 -04:00
b0017d134f removed references to vlc 2022-09-07 20:02:36 -04:00
796d05925a removed policies and preferences 2022-09-06 20:43:33 -04:00
86ab5edf4b add path prefix #196. 2022-09-01 18:22:01 +02:00
1edeb717b1 added opera 2022-08-29 21:59:07 -04:00
43fe80c82e update chromium policies. 2022-08-20 23:27:23 +02:00
8c9c348615 fix link to neko-rooms. 2022-08-04 01:19:29 +02:00
32f4dc5f2a npm upgrade. 2022-07-31 12:26:33 +02:00
523289523d go mod upgrade. 2022-07-31 12:21:26 +02:00
45b0657858 update mux docs. 2022-07-31 12:19:05 +02:00
629369487c add funding. 2022-07-31 12:18:59 +02:00
3c17988b24 Updated Readme to include details about url args. (#188)
Please consider adding more info on the main page as the url args are really useful.
2022-07-30 20:50:35 +02:00
2ae54dea64 Merge pull request #186 from m1k1o/vivaldi
New Vivaldi browser
2022-06-18 21:29:29 +02:00
45b1aec1d9 add vivaldi browser. 2022-06-18 21:27:09 +02:00
861f4b02d1 Merge branch 'master' into vivaldi 2022-06-18 21:24:47 +02:00
e91836e9bb add extensions. 2022-06-18 21:24:07 +02:00
6882d102cc Merge branch 'master' of github.com:Xeddius/neko into vivaldi 2022-06-18 21:23:50 +02:00
6114b5b06b Install latest Ungoogled Chromium extensions at build time (#183)
Signed-off-by: Aaron <admin@datahoarder.dev>
2022-06-18 14:20:35 +02:00
934ac9d4e0 docs(configuration): fix typo (#184) 2022-06-18 12:54:39 +02:00
429122574f Update Ungoogled Chromium extensions (#181)
Signed-off-by: Aaron <admin@datahoarder.dev>
2022-06-17 22:58:18 +02:00
fe5a2f9ee7 version 2.6. 2022-06-17 21:15:57 +02:00
7d71fd2bc6 add chinese fonts, fixes #179. 2022-06-17 21:14:45 +02:00
9c4404963b lint fix. 2022-06-16 21:50:36 +02:00
c15daea875 Added simplified chinese as language (#176) 2022-06-08 09:04:39 +02:00
25aab1d7af update preferences.json 2022-06-03 13:58:29 -04:00
c054df072b edit profile 2022-06-03 13:57:51 -04:00
96fd19b178 remove default profile. 2022-06-02 21:03:17 +02:00
7ec0dea7bf edit preferences. 2022-06-02 21:02:44 +02:00
d420c48f5f fix openbox. 2022-06-02 21:02:27 +02:00
2c133599bd Update Dockerfile 2022-06-02 01:50:30 -04:00
b88e3e349a Added Vivaldi Image
Vivaldi browser for n.eko.
It's currently using a dummy "default" profile.
I'm working on contacting a developer to see if we can get some kind of preferences.json support and possibly a link to latest builds for ease of integration/updates.
2022-06-02 01:45:38 -04:00
b4e3bd6d1f github actions disable fail-fast. 2022-06-01 23:04:28 +02:00
676f36c973 upgrade guacamole keyboard, #174. 2022-06-01 22:28:09 +02:00
c48309b648 upgrade to go 1.18. 2022-05-27 23:40:19 +02:00
2aec417fa8 upgrade golang deps. 2022-05-27 23:32:00 +02:00
f97ed8a65b go mod upgrade. 2022-05-14 18:58:09 +02:00
1c1f638be7 build fail if any command fails. 2022-05-14 18:56:51 +02:00
3dce6b1204 fix cgo flags. 2022-05-14 18:56:09 +02:00
58b2812eaa change pulseaudio log level. 2022-05-14 18:54:38 +02:00
5a42e06510 update changelog. 2022-05-14 18:53:22 +02:00
4a9342bc50 upgrade sweetalert2. 2022-05-14 18:51:55 +02:00
c497b7325f npm upgrade + bump fontawesome version. 2022-05-14 18:13:56 +02:00
072d294468 change vp8enc pipeline attributes #156. 2022-04-30 19:45:59 +02:00
0062fc28aa overlay focus only when hosting. 2022-04-30 19:34:12 +02:00
Bad
b963279296 (typo fix) Web RTC -> WebRTC (#168)
how long has this been here 💀
2022-04-07 13:18:14 +02:00
181 changed files with 30358 additions and 3135 deletions

View File

@ -1,7 +1,7 @@
# #
# STAGE 1: SERVER # STAGE 1: SERVER
# #
FROM golang:1.17-bullseye as server FROM golang:1.19-bullseye as server
WORKDIR /src WORKDIR /src
# #
@ -32,7 +32,7 @@ RUN go get -v -t -d . && go build -o bin/neko cmd/neko/main.go
# #
# STAGE 2: CLIENT # STAGE 2: CLIENT
# #
FROM node:14-bullseye-slim as client FROM node:18-bullseye-slim as client
WORKDIR /src WORKDIR /src
# #
@ -61,11 +61,6 @@ ARG USER_UID=1000
ARG USER_GID=$USER_UID ARG USER_GID=$USER_UID
RUN set -eux; \ 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; \ apt-get update; \
# #
# install dependencies # install dependencies
@ -73,16 +68,18 @@ RUN set -eux; \
apt-get install -y --no-install-recommends pulseaudio dbus-x11 xserver-xorg-video-dummy; \ 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; \ apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6; \
# #
# intel driver + vaapi # gst
apt-get install -y --no-install-recommends intel-media-va-driver-non-free libva2 vainfo; \
#
# gst + vaapi plugin
apt-get install -y --no-install-recommends libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ 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-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio; \
gstreamer1.0-vaapi ;\
# #
# fonts # install fonts
apt-get install -y --no-install-recommends fonts-takao-mincho; \ 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 # create a non-root user
groupadd --gid $USER_GID $USERNAME; \ groupadd --gid $USER_GID $USERNAME; \
@ -116,7 +113,6 @@ COPY .docker/base/dbus /usr/bin/dbus
COPY .docker/base/default.pa /etc/pulse/default.pa COPY .docker/base/default.pa /etc/pulse/default.pa
COPY .docker/base/supervisord.conf /etc/neko/supervisord.conf COPY .docker/base/supervisord.conf /etc/neko/supervisord.conf
COPY .docker/base/xorg.conf /etc/neko/xorg.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 # set default envs
@ -125,7 +121,6 @@ ENV DISPLAY=:99.0
ENV NEKO_PASSWORD=neko ENV NEKO_PASSWORD=neko
ENV NEKO_PASSWORD_ADMIN=admin ENV NEKO_PASSWORD_ADMIN=admin
ENV NEKO_BIND=:8080 ENV NEKO_BIND=:8080
ENV RENDER_GID=
# #
# copy static files from previous stages # copy static files from previous stages

View File

@ -1,7 +1,7 @@
# #
# STAGE 1: SERVER # STAGE 1: SERVER
# #
FROM arm32v7/golang:1.17-buster as server FROM golang:1.19-bullseye as server
WORKDIR /src WORKDIR /src
# #
@ -13,7 +13,7 @@ RUN set -eux; apt-get update; \
# install libclipboard # install libclipboard
set -eux; \ set -eux; \
cd /tmp; \ cd /tmp; \
git clone https://github.com/jtanx/libclipboard; \ git clone --depth=1 https://github.com/jtanx/libclipboard; \
cd libclipboard; \ cd libclipboard; \
cmake .; \ cmake .; \
make -j4; \ make -j4; \
@ -32,7 +32,7 @@ RUN go get -v -t -d . && go build -o bin/neko cmd/neko/main.go
# #
# STAGE 2: CLIENT # STAGE 2: CLIENT
# #
FROM node:14-buster-slim as client FROM node:18-bullseye-slim as client
# install dependencies # install dependencies
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \
@ -53,7 +53,7 @@ RUN npm run build
# #
# STAGE 3: RUNTIME # STAGE 3: RUNTIME
# #
FROM arm32v7/debian:buster-slim FROM debian:bullseye-slim
# #
# avoid warnings by switching to noninteractive # avoid warnings by switching to noninteractive
@ -65,19 +65,27 @@ ARG USERNAME=neko
ARG USER_UID=1000 ARG USER_UID=1000
ARG USER_GID=$USER_UID ARG USER_GID=$USER_UID
# RUN set -eux; \
# install dependencies apt-get update; \
RUN set -eux; apt-get update; \ #
# install dependencies
apt-get install -y --no-install-recommends wget ca-certificates supervisor; \ 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 pulseaudio dbus-x11 xserver-xorg-video-dummy; \
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx5; \ apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6; \
# #
# gst # gst
apt-get install -y --no-install-recommends libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ 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-omx; \ gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio \
gstreamer1.0-omx; \
# #
# fonts # install fonts
apt-get install -y --no-install-recommends fonts-takao-mincho; \ 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 # create a non-root user
groupadd --gid $USER_GID $USERNAME; \ groupadd --gid $USER_GID $USERNAME; \
@ -131,4 +139,3 @@ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
# #
# run neko # run neko
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"] CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View File

@ -0,0 +1,146 @@
#
# STAGE 1: SERVER
#
FROM golang:1.19-bullseye as server
WORKDIR /src
#
# install dependencies
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends git cmake make libx11-dev libxrandr-dev libxtst-dev \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly; \
#
# install libclipboard
set -eux; \
cd /tmp; \
git clone --depth=1 https://github.com/jtanx/libclipboard; \
cd libclipboard; \
cmake .; \
make -j4; \
make install; \
rm -rf /tmp/libclipboard; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# build server
COPY server/ .
RUN go get -v -t -d . && go build -o bin/neko cmd/neko/main.go
#
# STAGE 2: CLIENT
#
FROM node:18-bullseye-slim as client
WORKDIR /src
#
# install dependencies
COPY client/package*.json ./
RUN npm install
#
# build client
COPY client/ .
RUN npm run build
#
# STAGE 3: RUNTIME
#
FROM debian:bullseye-slim
#
# avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive
#
# set custom user
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
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; \
#
# intel driver + vaapi
apt-get install -y --no-install-recommends intel-media-va-driver-non-free libva2 vainfo; \
#
# 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; \
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
adduser $USERNAME audio; \
adduser $USERNAME video; \
adduser $USERNAME pulse; \
#
# setup pulseaudio
mkdir -p /home/$USERNAME/.config/pulse/; \
echo "default-server=unix:/tmp/pulseaudio.socket" > /home/$USERNAME/.config/pulse/client.conf; \
#
# workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
mkdir /tmp/.X11-unix; \
chmod 1777 /tmp/.X11-unix; \
chown $USERNAME /tmp/.X11-unix/; \
#
# make directories for neko
mkdir -p /etc/neko /var/www /var/log/neko; \
chmod 1777 /var/log/neko; \
chown $USERNAME /var/log/neko/; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy config files
COPY .docker/base/dbus /usr/bin/dbus
COPY .docker/base/default.pa /etc/pulse/default.pa
COPY .docker/base/intel/supervisord.conf /etc/neko/supervisord.conf
COPY .docker/base/xorg.conf /etc/neko/xorg.conf
COPY .docker/base/intel/add-render-group.sh /usr/bin/add-render-group.sh
#
# set default envs
ENV USER=$USERNAME
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
COPY --from=server /src/bin/neko /usr/bin/neko
COPY --from=client /src/dist/ /var/www
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_BIND#*:}/health || exit 1
#
# run neko
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View File

@ -0,0 +1,69 @@
[supervisord]
nodaemon=true
user=root
pidfile=/var/run/supervisord.pid
logfile=/dev/null
logfile_maxbytes=0
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
autorestart=true
priority=100
user=root
stdout_logfile=/var/log/neko/dbus.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:x-server]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s"
command=/usr/bin/X -config /etc/neko/xorg.conf %(ENV_DISPLAY)s
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/xorg.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:pulseaudio]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/pulseaudio --log-level=info --disallow-module-loading --disallow-exit --exit-idle-time=-1
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/pulseaudio.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:neko]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/neko serve --static "/var/www"
stopsignal=INT
stopwaitsecs=5
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/neko.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -9,19 +9,6 @@ loglevel=debug
[include] [include]
files=/etc/neko/supervisord/*.conf 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] [program:dbus]
environment=HOME="/root",USER="root" environment=HOME="/root",USER="root"
command=/usr/bin/dbus command=/usr/bin/dbus
@ -46,7 +33,7 @@ redirect_stderr=true
[program:pulseaudio] [program:pulseaudio]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" 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 autorestart=true
priority=300 priority=300
user=%(ENV_USER)s user=%(ENV_USER)s

View File

@ -13,7 +13,7 @@
<applications> <applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") --> <!-- 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> <decor>no</decor>
<maximized>true</maximized> <maximized>true</maximized>
<focus>yes</focus> <focus>yes</focus>

View File

@ -1,6 +1,4 @@
{ {
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false, "AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false, "AutofillCreditCardEnabled": false,
"BrowserSignin": 0, "BrowserSignin": 0,
@ -20,7 +18,10 @@
"PromptForDownloadLocation": false, "PromptForDownloadLocation": false,
"BookmarkBarEnabled": false, "BookmarkBarEnabled": false,
"PasswordManagerEnabled": false, "PasswordManagerEnabled": false,
"URLBlacklist": [ "URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*", "file://*",
"chrome://policy" "chrome://policy"
], ],
@ -28,11 +29,11 @@
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx", "cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx" "mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
], ],
"ExtensionInstallWhitelist": [ "ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm", "cjpalhdlnbpafiamejdnhcphjbkeiagm",
"mnjggcdmjocbbbhaepdhchncahnbgone" "mnjggcdmjocbbbhaepdhchncahnbgone"
], ],
"ExtensionInstallBlacklist": [ "ExtensionInstallBlocklist": [
"*" "*"
] ]
} }

View File

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
set -ex
cd "$(dirname "$0")" cd "$(dirname "$0")"
BASE="${PWD}/../" BASE="${PWD}/../"
@ -76,6 +77,21 @@ build_arm() {
fi fi
} }
build_intel() {
if [ "$1" = "base" ]
then
# build intel base
docker build -t "${BUILD_IMAGE}:intel-base" -f base/Dockerfile.intel "${BASE}"
elif [ -f "$1/Dockerfile.intel" ]
then
# build dedicated intel image
docker build -t "${BUILD_IMAGE}:intel-$1" --build-arg="BASE_IMAGE=${BUILD_IMAGE}:intel-base" -f "$1/Dockerfile.intel" "$1/"
else
# try to build intel image with common Dockerfile
docker build -t "${BUILD_IMAGE}:intel-$1" --build-arg="BASE_IMAGE=${BUILD_IMAGE}:intel-base" -f "$1/Dockerfile" "$1/"
fi
}
case $1 in case $1 in
client) build_client;; client) build_client;;
server) build_server;; server) build_server;;
@ -83,6 +99,9 @@ case $1 in
# build arm- images # build arm- images
arm-*) build_arm "${1#arm-}";; arm-*) build_arm "${1#arm-}";;
# build intel- images
intel-*) build_intel "${1#intel-}";;
# build images # build images
*) build "$1";; *) build "$1";;
esac esac

View File

@ -4,7 +4,7 @@ FROM $BASE_IMAGE
# #
# install neko chromium # install neko chromium
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \
# TODO: Bring back DRM support with arm32v7/debian:buster-slim image. # TODO: Bring back DRM support.
apt-get install -y --no-install-recommends chromium openbox; \ apt-get install -y --no-install-recommends chromium openbox; \
# #
# clean up # clean up

View File

@ -13,7 +13,7 @@
<applications> <applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") --> <!-- 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> <decor>no</decor>
<maximized>true</maximized> <maximized>true</maximized>
<focus>yes</focus> <focus>yes</focus>

View File

@ -1,6 +1,4 @@
{ {
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false, "AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false, "AutofillCreditCardEnabled": false,
"BrowserSignin": 0, "BrowserSignin": 0,
@ -21,7 +19,10 @@
"BookmarkBarEnabled": false, "BookmarkBarEnabled": false,
"PasswordManagerEnabled": false, "PasswordManagerEnabled": false,
"BrowserLabsEnabled": false, "BrowserLabsEnabled": false,
"URLBlacklist": [ "URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*", "file://*",
"chrome://policy" "chrome://policy"
], ],
@ -29,11 +30,11 @@
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx", "cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx" "mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
], ],
"ExtensionInstallWhitelist": [ "ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm", "cjpalhdlnbpafiamejdnhcphjbkeiagm",
"mnjggcdmjocbbbhaepdhchncahnbgone" "mnjggcdmjocbbbhaepdhchncahnbgone"
], ],
"ExtensionInstallBlacklist": [ "ExtensionInstallBlocklist": [
"*" "*"
] ]
} }

View File

@ -13,7 +13,7 @@
<applications> <applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") --> <!-- 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> <decor>no</decor>
<maximized>true</maximized> <maximized>true</maximized>
<focus>yes</focus> <focus>yes</focus>

View File

@ -13,7 +13,7 @@
<applications> <applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") --> <!-- 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> <decor>no</decor>
<maximized>true</maximized> <maximized>true</maximized>
<focus>yes</focus> <focus>yes</focus>

View File

@ -1,6 +1,4 @@
{ {
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false, "AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false, "AutofillCreditCardEnabled": false,
"BrowserSignin": 0, "BrowserSignin": 0,
@ -20,7 +18,10 @@
"PromptForDownloadLocation": false, "PromptForDownloadLocation": false,
"BookmarkBarEnabled": false, "BookmarkBarEnabled": false,
"PasswordManagerEnabled": false, "PasswordManagerEnabled": false,
"URLBlacklist": [ "URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*", "file://*",
"chrome://policy" "chrome://policy"
], ],
@ -28,11 +29,11 @@
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx", "cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx" "mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
], ],
"ExtensionInstallWhitelist": [ "ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm", "cjpalhdlnbpafiamejdnhcphjbkeiagm",
"mnjggcdmjocbbbhaepdhchncahnbgone" "mnjggcdmjocbbbhaepdhchncahnbgone"
], ],
"ExtensionInstallBlacklist": [ "ExtensionInstallBlocklist": [
"*" "*"
] ]
} }

View File

@ -13,7 +13,7 @@
<applications> <applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") --> <!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
<application class="Microsoft-edge*" name="microsoft-edge*"> <application class="Microsoft-edge*" name="microsoft-edge*" role="browser">
<decor>no</decor> <decor>no</decor>
<maximized>true</maximized> <maximized>true</maximized>
<focus>yes</focus> <focus>yes</focus>

View File

@ -1,6 +1,4 @@
{ {
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false, "AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false, "AutofillCreditCardEnabled": false,
"BrowserSignin": 0, "BrowserSignin": 0,
@ -20,7 +18,10 @@
"PromptForDownloadLocation": false, "PromptForDownloadLocation": false,
"BookmarkBarEnabled": false, "BookmarkBarEnabled": false,
"PasswordManagerEnabled": false, "PasswordManagerEnabled": false,
"URLBlacklist": [ "URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*", "file://*",
"edge://policy" "edge://policy"
], ],
@ -28,11 +29,11 @@
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx", "cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx" "mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
], ],
"ExtensionInstallWhitelist": [ "ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm", "cjpalhdlnbpafiamejdnhcphjbkeiagm",
"mnjggcdmjocbbbhaepdhchncahnbgone" "mnjggcdmjocbbbhaepdhchncahnbgone"
], ],
"ExtensionInstallBlacklist": [ "ExtensionInstallBlocklist": [
"*" "*"
] ]
} }

31
.docker/opera/Dockerfile Normal file
View 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

763
.docker/opera/openbox.xml Normal file
View 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="Opera" name="Opera" 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>

View File

@ -0,0 +1,22 @@
[program:opera]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)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/opera.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

View File

@ -11,6 +11,7 @@ RUN set -eux; apt-get update; \
echo "Downloading $DOWNLOAD_URI"; \ echo "Downloading $DOWNLOAD_URI"; \
curl -sSL -o /tmp/tor.tar.xz "https://www.torproject.org/$DOWNLOAD_URI"; \ curl -sSL -o /tmp/tor.tar.xz "https://www.torproject.org/$DOWNLOAD_URI"; \
tar -xvJf /tmp/tor.tar.xz -C /opt; \ tar -xvJf /tmp/tor.tar.xz -C /opt; \
mv /opt/tor-browser* /opt/tor-browser_en-US; \
chown -R neko:neko /opt/tor-browser_en-US/; \ chown -R neko:neko /opt/tor-browser_en-US/; \
rm -f /tmp/tor.tar.xz; \ rm -f /tmp/tor.tar.xz; \
# #

View File

@ -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 # install custom chromium build from woolyss with support for hevc/x265
SHELL ["/bin/bash", "-c"]
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends wget unzip libatk1.0-0 libatk-bridge2.0-0 libatomic1 \ 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; \ 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 # install widevine module
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \ 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; \ unzip -p /tmp/widevine.zip libwidevinecdm.so > /usr/lib/chromium/libwidevinecdm.so; \
chmod 644 /usr/lib/chromium/libwidevinecdm.so; \ chmod 644 /usr/lib/chromium/libwidevinecdm.so; \
rm /tmp/widevine.zip; \ 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 # clean up
apt-get --purge autoremove -y xz-utils jq; \ apt-get --purge autoremove -y xz-utils jq; \
apt-get clean -y; \ 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 preferences.json /usr/lib/chromium/master_preferences
COPY policies.json /etc/chromium/policies/managed/policies.json COPY policies.json /etc/chromium/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml COPY openbox.xml /etc/neko/openbox.xml
#
# copy extensions and policy files
COPY extensions /usr/share/chromium/extensions

View File

@ -1,4 +0,0 @@
{
"external_crx": "/usr/share/chromium/extensions/cjpalhdlnbpafiamejdnhcphjbkeiagm.crx",
"external_version": "1.38.6"
}

View File

@ -1,4 +0,0 @@
{
"external_crx": "/usr/share/chromium/extensions/mnjggcdmjocbbbhaepdhchncahnbgone.crx",
"external_version": "4.0.5"
}

View File

@ -13,7 +13,7 @@
<applications> <applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") --> <!-- 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> <decor>no</decor>
<maximized>true</maximized> <maximized>true</maximized>
<focus>yes</focus> <focus>yes</focus>

View File

@ -1,6 +1,4 @@
{ {
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false, "AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false, "AutofillCreditCardEnabled": false,
"BrowserSignin": 0, "BrowserSignin": 0,
@ -20,15 +18,18 @@
"PromptForDownloadLocation": false, "PromptForDownloadLocation": false,
"BookmarkBarEnabled": false, "BookmarkBarEnabled": false,
"PasswordManagerEnabled": false, "PasswordManagerEnabled": false,
"URLBlacklist": [ "URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*", "file://*",
"chrome://policy" "chrome://policy"
], ],
"ExtensionInstallWhitelist": [ "ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm", "cjpalhdlnbpafiamejdnhcphjbkeiagm",
"mnjggcdmjocbbbhaepdhchncahnbgone" "mnjggcdmjocbbbhaepdhchncahnbgone"
], ],
"ExtensionInstallBlacklist": [ "ExtensionInstallBlocklist": [
"*" "*"
] ]
} }

View 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
View 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>

View 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": [
"*"
]
}

View 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
}
}
}

View 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
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: [ m1k1o ]

View File

@ -1,4 +1,4 @@
name: "CI for builds" name: "build and push amd64 images to Docker Hub"
on: on:
push: push:
@ -47,8 +47,10 @@ jobs:
if: github.repository_owner == 'm1k1o' if: github.repository_owner == 'm1k1o'
needs: [ build-base ] needs: [ build-base ]
strategy: strategy:
# Will build all images even if some fail.
fail-fast: false
matrix: matrix:
tags: [ firefox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, tor-browser, remmina, vlc, xfce ] tags: [ firefox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, vivaldi, opera, tor-browser, remmina, vlc, xfce ]
env: env:
DOCKER_TAG: ${{ matrix.tags }} DOCKER_TAG: ${{ matrix.tags }}
steps: steps:

View File

@ -1,4 +1,4 @@
name: "CI for version tags" name: "amd64 images"
on: on:
push: push:
@ -8,6 +8,9 @@ on:
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: m1k1o/neko IMAGE_NAME: m1k1o/neko
TAG_PREFIX: ""
BASE_DOCKERFILE: Dockerfile
PLATFORMS: linux/amd64
jobs: jobs:
build-base: build-base:
@ -31,7 +34,7 @@ jobs:
uses: docker/metadata-action@v3 uses: docker/metadata-action@v3
id: meta id: meta
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/base images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
@ -49,8 +52,8 @@ jobs:
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: ./ context: ./
file: .docker/base/Dockerfile file: .docker/base/${{ env.BASE_DOCKERFILE }}
platforms: linux/amd64,linux/arm64 platforms: ${{ env.PLATFORMS }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
@ -63,31 +66,23 @@ jobs:
if: github.repository_owner == 'm1k1o' if: github.repository_owner == 'm1k1o'
needs: [ build-base ] needs: [ build-base ]
strategy: strategy:
# Will build all images even if some fail.
matrix: matrix:
include: include:
- tag: firefox - tag: firefox
platforms: linux/amd64,linux/arm64
- tag: chromium - tag: chromium
platforms: linux/amd64,linux/arm64
- tag: google-chrome - tag: google-chrome
platforms: linux/amd64
- tag: ungoogled-chromium - tag: ungoogled-chromium
platforms: linux/amd64,linux/arm64
- tag: microsoft-edge - tag: microsoft-edge
platforms: linux/amd64
- tag: brave - tag: brave
platforms: linux/amd64 - tag: vivaldi
- tag: opera
- tag: tor-browser - tag: tor-browser
platforms: linux/amd64,linux/arm64
- tag: remmina - tag: remmina
platforms: linux/amd64
- tag: vlc - tag: vlc
platforms: linux/amd64,linux/arm64
- tag: xfce - tag: xfce
platforms: linux/amd64,linux/arm64
env: env:
TAG_NAME: ${{ matrix.tag }} TAG_NAME: ${{ matrix.tag }}
PLATFORMS: ${{ matrix.platforms }}
steps: steps:
- -
name: Checkout name: Checkout
@ -103,11 +98,12 @@ jobs:
uses: docker/metadata-action@v3 uses: docker/metadata-action@v3
id: meta id: meta
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}${{ env.TAG_NAME }}
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=sha,format=long
- -
name: Log in to the Container registry name: Log in to the Container registry
uses: docker/login-action@v1 uses: docker/login-action@v1
@ -125,4 +121,4 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/base:sha-${{ github.sha }} BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base:sha-${{ github.sha }}

126
.github/workflows/ghcr-arm.yml vendored Normal file
View File

@ -0,0 +1,126 @@
name: "arm64v8 and arm32v7 images"
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: m1k1o/neko
TAG_PREFIX: arm-
BASE_DOCKERFILE: Dockerfile.arm
PLATFORMS: linux/arm64,linux/arm/v7
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 }}/${{ env.TAG_PREFIX }}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/${{ env.BASE_DOCKERFILE }}
platforms: ${{ env.PLATFORMS }}
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
dockerfile: Dockerfile.arm
- tag: chromium
dockerfile: Dockerfile.arm
- tag: ungoogled-chromium
dockerfile: Dockerfile
- tag: tor-browser
dockerfile: Dockerfile
- tag: vlc
dockerfile: Dockerfile
- tag: xfce
dockerfile: Dockerfile
env:
TAG_NAME: ${{ matrix.tag }}
DOCKERFILE: ${{ matrix.dockerfile }}
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_PREFIX }}${{ env.TAG_NAME }}
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: .docker/${{ env.TAG_NAME }}
file: .docker/${{ env.TAG_NAME }}/${{ env.DOCKERFILE }}
platforms: ${{ env.PLATFORMS }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base:sha-${{ github.sha }}

125
.github/workflows/ghcr-intel.yml vendored Normal file
View File

@ -0,0 +1,125 @@
name: "intel gpu supported images"
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: m1k1o/neko
TAG_PREFIX: intel-
BASE_DOCKERFILE: Dockerfile.intel
PLATFORMS: linux/amd64
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 }}/${{ env.TAG_PREFIX }}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/${{ env.BASE_DOCKERFILE }}
platforms: ${{ env.PLATFORMS }}
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
- tag: chromium
- tag: google-chrome
- tag: ungoogled-chromium
- tag: microsoft-edge
- tag: brave
- tag: vivaldi
- tag: opera
- tag: tor-browser
- tag: remmina
- tag: vlc
- tag: xfce
env:
TAG_NAME: ${{ matrix.tag }}
DOCKERFILE: ${{ matrix.dockerfile }}
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_PREFIX }}${{ env.TAG_NAME }}
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: .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 }}/${{ env.TAG_PREFIX }}base:sha-${{ github.sha }}

4
.gitignore vendored
View File

@ -23,10 +23,6 @@ pids
*.seed *.seed
*.pid.lock *.pid.lock
# Lock files
yarn.lock
package-lock.json
# TypeScript incremental compilation cache # TypeScript incremental compilation cache
*.tsbuildinfo *.tsbuildinfo

124
README.md
View File

@ -1,40 +1,126 @@
<div align="center"> <div align="center">
<a href="https://github.com/m1k1o/neko" title="Neko's Github repository."> <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> </a>
<p align="center"> <p align="center">
<img src="https://img.shields.io/github/v/release/m1k1o/neko" alt="release"> <a href="https://github.com/m1k1o/neko/releases">
<img src="https://img.shields.io/github/license/m1k1o/neko" alt="license"> <img src="https://img.shields.io/github/v/release/m1k1o/neko" alt="release">
<img src="https://img.shields.io/docker/pulls/m1k1o/neko" alt="pulls"> </a>
<img src="https://img.shields.io/github/issues/m1k1o/neko" alt="issues"> <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"> <a href="https://discord.gg/3U6hWpC">
<img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord"> <img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord">
</a> </a>
<a href="https://github.com/m1k1o/neko/actions"> <a href="https://github.com/m1k1o/neko/actions">
<img src="https://github.com/m1k1o/neko/actions/workflows/build.yml/badge.svg" alt="build"> <img src="https://github.com/m1k1o/neko/actions/workflows/ghcr-amd.yml/badge.svg" alt="build">
</a> </a>
</p> </p>
<br/> <img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/intro.gif" width="650" height="auto"/>
<br/>
<img src="https://i.imgur.com/ZSzbQr7.gif" width="650" height="auto"/>
<br/>
<br/>
</div> </div>
# n.eko # n.eko
This app uses Web RTC to stream a desktop inside of a docker container, original author made this because [rabb.it](https://en.wikipedia.org/wiki/Rabb.it) went under and his internet could not handle streaming and discord kept crashing when his friend attempted to. He just wanted to watch anime with his friends ლ(ಠ益ಠლ) so he started digging throughout the internet and found a few *kinda* clones, but none of them had the virtual browser, then he found [Turtus](https://github.com/Khauri/Turtus) and he was able to figure out the rest. Welcome to Neko, a self-hosted virtual browser that runs in Docker and uses WebRTC technology. Neko is a powerful tool that allows you to **run a fully-functional browser in a virtual environment**, giving you the ability to **access the internet securely and privately from anywhere**. With Neko, you can browse the web, **run applications**, and perform other tasks just as you would on a regular browser, all within a **secure and isolated environment**. Whether you are a developer looking to test web applications, a **privacy-conscious user seeking a secure browsing experience**, or simply someone who wants to take advantage of the **convenience and flexibility of a virtual browser**, Neko is the perfect solution.
In addition to its security and privacy features, Neko offers the **ability for multiple users to access it simultaneously**. This makes it an ideal solution for teams or organizations that need to share access to a browser, as well as for individuals who want to use **multiple devices to access the same virtual environment**. With Neko, you can **easily and securely share access to a browser with others**, without having to worry about maintaining separate configurations or settings. Whether you need to **collaborate on a project**, access shared resources, or simply want to **share access to a browser with friends or family**, Neko makes it easy to do so.
Neko is also a great tool for **hosting watch parties** and interactive presentations. With its virtual browser capabilities, Neko allows you to host watch parties and presentations that are **accessible from anywhere**, without the need for in-person gatherings. This makes it easy to **stay connected with friends and colleagues**, even when you are unable to meet in person. With Neko, you can easily host a watch party or give an **interactive presentation**, whether it's for leisure or work. Simply invite your guests to join the virtual environment, and you can share the screen and **interact with them in real-time**.
## About
This app uses WebRTC to stream a desktop inside of a docker container, original author made this because [rabb.it](https://en.wikipedia.org/wiki/Rabb.it) went under and his internet could not handle streaming and discord kept crashing when his friend attempted to. He just wanted to watch anime with his friends ლ(ಠ益ಠლ) so he started digging throughout the internet and found a few *kinda* clones, but none of them had the virtual browser, then he found [Turtus](https://github.com/Khauri/Turtus) and he was able to figure out the rest.
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. 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 ### Features
* Text Chat (With basic markdown support, discord flavor) * 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)) * Clipboard synchronization (on [supported browsers](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText))
* Emote overlay * Emote overlay
* Ignore user (chat and emotes) * Ignore user (chat and emotes)
* Persistent settings * 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? ### Why n.eko?
@ -42,13 +128,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. ***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). 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/) * [Getting Started](https://neko.m1k1o.net/#/getting-started/)
* [Quick Start](https://neko.m1k1o.net/#/getting-started/quick-start) * [Quick Start](https://neko.m1k1o.net/#/getting-started/quick-start)
@ -62,6 +148,10 @@ It also offers zero-knowledge [installation script](https://github.com/m1k1o/nek
* [Technologies](https://neko.m1k1o.net/#/technologies) * [Technologies](https://neko.m1k1o.net/#/technologies)
* [Changelog](https://neko.m1k1o.net/#/changelog) * [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. 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).

21271
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "neko-client", "name": "neko-client",
"version": "2.0.0", "version": "2.5.0",
"description": "Client for neko streaming server", "description": "Client for neko streaming server",
"license": "Apache License 2.0", "license": "Apache License 2.0",
"author": "Nurdism <https://github.com/nurdism>", "author": "Nurdism <https://github.com/nurdism>",
@ -20,50 +20,50 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^6.2.0",
"animejs": "^3.2.0", "animejs": "^3.2.0",
"axios": "^0.21.4", "axios": "^1.2.3",
"date-fns": "^2.28.0", "date-fns": "^2.29.3",
"emoji-datasource": "^6.0.1", "emoji-datasource": "^6.0.1",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"simple-markdown": "^0.7.2", "simple-markdown": "^0.7.2",
"sweetalert2": "^10.15.7", "sweetalert2": "11.4.8",
"typed-vuex": "^0.1.21", "typed-vuex": "^0.1.21",
"v-tooltip": "^2.0.3", "v-tooltip": "^2.0.3",
"vue": "^2.6.14", "vue": "^2.7.13",
"vue-class-component": "^7.2.6", "vue-class-component": "^7.2.6",
"vue-clickaway": "^2.2.2", "vue-clickaway": "^2.2.2",
"vue-context": "^5.2.0", "vue-context": "^5.2.0",
"vue-i18n": "^8.27.0", "vue-i18n": "^8.27.2",
"vue-notification": "^1.3.20", "vue-notification": "^1.3.20",
"vue-property-decorator": "^9.1.2", "vue-property-decorator": "^9.1.2",
"vuex": "^3.5.1" "vuex": "^3.5.1"
}, },
"devDependencies": { "devDependencies": {
"@types/animejs": "^3.1.4", "@types/animejs": "^3.1.6",
"@types/node": "^14.18.9", "@types/node": "^18.11.18",
"@types/vue": "^2.0.0", "@types/vue": "^2.0.0",
"@types/vue-clickaway": "^2.2.0", "@types/vue-clickaway": "^2.2.0",
"@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/eslint-plugin": "^5.0.8",
"@typescript-eslint/parser": "^4.33.0", "@typescript-eslint/parser": "^5.0.8",
"@vue/cli-plugin-babel": "^4.5.15", "@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^4.5.15", "@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-typescript": "^4.5.15", "@vue/cli-plugin-typescript": "^5.0.8",
"@vue/cli-plugin-vuex": "^4.5.15", "@vue/cli-plugin-vuex": "^5.0.8",
"@vue/cli-service": "^4.5.15", "@vue/cli-service": "^5.0.8",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0", "@vue/eslint-config-typescript": "^11.0.2",
"core-js": "^3.20.3", "core-js": "^3.26.0",
"emojilib": "^3.0.4", "emojilib": "^3.0.7",
"eslint": "^6.8.0", "eslint": "^8.32.0",
"eslint-plugin-prettier": "^3.4.1", "eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-vue": "^7.20.0", "eslint-plugin-vue": "^9.0.0",
"prettier": "^2.5.1", "prettier": "^2.7.1",
"sass": "^1.49.0", "sass": "^1.55.0",
"sass-loader": "^10.2.1", "sass-loader": "^10.3.1",
"ts-node": "^9.1.1", "ts-node": "^9.1.1",
"typescript": "^4.5.5", "typescript": "^4.8.4",
"vue-template-compiler": "^2.6.14" "vue-template-compiler": "^2.7.13"
} }
} }

View File

@ -2,7 +2,7 @@
@use "sass:math"; @use "sass:math";
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
$fa-font-size-base: 16px; $fa-font-size-base: 16px;
$fa-font-display: auto; $fa-font-display: auto;
$fa-css-prefix: fa; $fa-css-prefix: fa;
@ -13,7 +13,7 @@ $fa-fw-width: math.div(20em, 16);
$fa-primary-opacity: 1; $fa-primary-opacity: 1;
$fa-secondary-opacity: .4; $fa-secondary-opacity: .4;
$fa-family-default: 'Font Awesome 5 Free'; $fa-family-default: 'Font Awesome 6 Free';
// Import FA source files // Import FA source files
@import "~@fortawesome/fontawesome-free/scss/brands"; @import "~@fortawesome/fontawesome-free/scss/brands";

View File

@ -1,44 +1,16 @@
$swal2-white: #fff; @import '~sweetalert2/src/variables';
$swal2-black: #000;
$swal2-outline-color: transparent; $swal2-outline-color: transparent;
// CONTAINER // POPUP
$swal2-container-padding: .625em;
// BOX MODEL
$swal2-width: 32em;
$swal2-padding: 1.25em; $swal2-padding: 1.25em;
$swal2-border: none;
$swal2-border-radius: .3125em; $swal2-border-radius: .3125em;
$swal2-box-shadow: #d9d9d9;
// ANIMATIONS
$swal2-show-animation: swal2-show .3s;
$swal2-hide-animation: swal2-hide .15s forwards;
// BACKGROUND // BACKGROUND
$swal2-background: $background-secondary; $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 // ICONS
$swal2-icon-size: 5em;
$swal2-icon-animations: true;
$swal2-icon-margin: 1.25em auto 1.875em; $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 // IMAGE
$swal2-image-margin: 1.25em auto; $swal2-image-margin: 1.25em auto;
@ -46,143 +18,53 @@ $swal2-image-margin: 1.25em auto;
// TITLE // TITLE
$swal2-title-margin: 0 0 .4em; $swal2-title-margin: 0 0 .4em;
$swal2-title-color: $interactive-hover; $swal2-title-color: $interactive-hover;
$swal2-title-font-size: 1.875em;
// CONTENT // HTML CONTAINER
$swal2-content-justify-content: center; $swal2-html-container-margin: 0;
$swal2-content-margin: 0; $swal2-html-container-color: $interactive-hover;
$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;
// INPUT // INPUT
$swal2-input-margin: 1em auto; $swal2-input-margin: 1em auto;
$swal2-input-width: 100%; $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-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-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 // PROGRESS STEPS
$swal2-progress-steps-background: inherit; $swal2-progress-steps-background: inherit;
$swal2-progress-steps-margin: 0 0 1.25em; $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-background: #3085d6;
$swal2-active-step-color: $swal2-white;
// FOOTER // FOOTER
$swal2-footer-margin: 1.25em 0 0; $swal2-footer-margin: 1.25em 0 0;
$swal2-footer-padding: 1em 0 0; $swal2-footer-padding: 1em 0 0;
$swal2-footer-border-color: #eee;
$swal2-footer-color: lighten($swal2-black, 33); $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 // 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-transition: color .1s ease-out;
$swal2-close-button-border: none;
$swal2-close-button-border-radius: 0; $swal2-close-button-border-radius: 0;
$swal2-close-button-outline: initial; $swal2-close-button-outline: initial;
$swal2-close-button-background: transparent;
$swal2-close-button-color: lighten($swal2-black, 80); $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 // ACTIONS
$swal2-actions-flex-wrap: wrap;
$swal2-actions-align-items: center;
$swal2-actions-justify-content: center;
$swal2-actions-width: 100%; $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 // 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; $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 // 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-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-width: auto;
$swal2-toast-padding: .625em; $swal2-toast-padding: .625em;
$swal2-toast-title-margin: 0 .6em; $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"; @import "~sweetalert2/src/sweetalert2.scss";

View File

@ -132,8 +132,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Vue } from 'vue-property-decorator'
import md, { HtmlOutputRule } from 'simple-markdown'
@Component({ name: 'neko-about' }) @Component({ name: 'neko-about' })
export default class extends Vue { export default class extends Vue {

View File

@ -147,8 +147,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Vue } from 'vue-property-decorator'
import { get, set } from '~/utils/localstorage'
@Component({ name: 'neko-connect' }) @Component({ name: 'neko-connect' })
export default class extends Vue { export default class extends Vue {

View File

@ -132,7 +132,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Ref, Vue } from 'vue-property-decorator'
import { Member } from '~/neko/types' import { Member } from '~/neko/types'
// @ts-ignore // @ts-ignore
@ -229,11 +229,11 @@
} }
} }
adminRelease(member: Member) { adminRelease() {
this.$accessor.remote.adminRelease() this.$accessor.remote.adminRelease()
} }
adminControl(member: Member) { adminControl() {
this.$accessor.remote.adminControl() this.$accessor.remote.adminControl()
} }

View File

@ -212,7 +212,7 @@
&:before { &:before {
color: $background-tertiary; color: $background-tertiary;
font-weight: 900; font-weight: 900;
font-family: 'Font Awesome 5 Free'; font-family: 'Font Awesome 6 Free';
content: '\f3c1'; content: '\f3c1';
font-size: 8px; font-size: 8px;
line-height: 18px; line-height: 18px;

View File

@ -90,7 +90,7 @@
&::before { &::before {
content: '\f002'; content: '\f002';
font-weight: 900; font-weight: 900;
font-family: 'Font Awesome 5 Free'; font-family: 'Font Awesome 6 Free';
position: absolute; position: absolute;
width: 15px; width: 15px;
height: 15px; height: 15px;
@ -287,9 +287,9 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Ref, Vue } from 'vue-property-decorator'
import { directive as onClickaway } from 'vue-clickaway' import { directive as onClickaway } from 'vue-clickaway'
import { get, set } from '../utils/localstorage' import { get } from '../utils/localstorage'
@Component({ @Component({
name: 'neko-emoji', name: 'neko-emoji',
@ -356,7 +356,7 @@
this.waitingForPaint = false this.waitingForPaint = false
let scrollTop = this._scroll.scrollTop let scrollTop = this._scroll.scrollTop
let active = 0 let active = 0
for (const [i, group] of this.groups.entries()) { for (const [i] of this.groups.entries()) {
let component = this._groups[i] let component = this._groups[i]
if (component && component.offsetTop > scrollTop) { if (component && component.offsetTop > scrollTop) {
break break
@ -368,7 +368,7 @@
} }
} }
onMouseExit(event: MouseEvent, emoji: string) { onMouseExit() {
this.hovered = '' this.hovered = ''
} }
@ -382,7 +382,7 @@
this.$emit('picked', emoji) this.$emit('picked', emoji)
} }
onClickAway(event: MouseEvent) { onClickAway() {
this.$emit('done') this.$emit('done')
} }
} }

View 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.total && 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>

View File

@ -5,6 +5,14 @@
<span><b>n</b>.eko</span> <span><b>n</b>.eko</span>
</a> </a>
<ul class="menu"> <ul class="menu">
<li>
<button class="btn" @click="startShareScreen" v-if="!mediaStream">
START SCREEN SHARE
</button>
<button class="btn" @click="stopShareScreen" v-else>
STOP SCREEN SHARE
</button>
</li>
<li> <li>
<i <i
:class="[{ disabled: !admin }, { locked: isLocked('control') }, 'fas', 'fa-mouse']" :class="[{ disabled: !admin }, { locked: isLocked('control') }, 'fas', 'fa-mouse']"
@ -31,6 +39,19 @@
}" }"
/> />
</li> </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> <li>
<span v-if="showBadge" class="badge">&bull;</span> <span v-if="showBadge" class="badge">&bull;</span>
<i class="fas fa-bars toggle" @click="toggleMenu" /> <i class="fas fa-bars toggle" @click="toggleMenu" />
@ -144,7 +165,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Vue } from 'vue-property-decorator'
import { AdminLockResource } from '~/neko/messages' import { AdminLockResource } from '~/neko/messages'
@Component({ name: 'neko-settings' }) @Component({ name: 'neko-settings' })
@ -169,26 +190,24 @@
return !this.side && this.readTexts != this.texts 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 readTexts: number = 0
toggleMenu() { toggleMenu() {
this.$accessor.client.toggleSide() this.$accessor.client.toggleSide()
this.readTexts = this.texts 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) { lockedTooltip(resource: AdminLockResource) {
if (this.admin) { if (this.admin) {
return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `unlock` : `lock`)) return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `unlock` : `lock`))
@ -196,5 +215,31 @@
return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `locked` : `unlocked`)) return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `locked` : `unlocked`))
} }
//
// Screen Share
//
mediaStream: MediaStream | null = null
mediaRtcpSender: RTCRtpSender | null = null
async startShareScreen() {
// get media stream from user's browser
this.mediaStream = await navigator.mediaDevices
.getDisplayMedia({
video: true,
audio: false,
})
const mediaTrack = this.mediaStream.getVideoTracks()[0];
this.mediaRtcpSender = this.$client.addTrack(mediaTrack, this.mediaStream)
}
async stopShareScreen() {
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop())
this.mediaStream = null
}
if (this.mediaRtcpSender) {
this.$client.removeTrack(this.mediaRtcpSender)
this.mediaRtcpSender = null
}
}
} }
</script> </script>

View File

@ -1,5 +1,5 @@
import md, { SingleNodeParserRule, HtmlOutputRule, defaultRules, State, Rules } from 'simple-markdown' import md, { SingleNodeParserRule, HtmlOutputRule, defaultRules, State, Rules } from 'simple-markdown'
import { Component, Watch, Vue, Prop } from 'vue-property-decorator' import { Component, Vue, Prop } from 'vue-property-decorator'
const { blockQuote, inlineCode, codeBlock, autolink, newline, escape, strong, text, link, url, em, u, br } = const { blockQuote, inlineCode, codeBlock, autolink, newline, escape, strong, text, link, url, em, u, br } =
defaultRules defaultRules

View File

@ -77,7 +77,7 @@
&.self { &.self {
&::before { &::before {
font-family: 'Font Awesome 5 Free'; font-family: 'Font Awesome 6 Free';
font-weight: 900; font-weight: 900;
content: '\f2bd'; content: '\f2bd';
background: $background-floating; background: $background-floating;
@ -97,7 +97,7 @@
&.admin { &.admin {
&::before { &::before {
display: block; display: block;
font-family: 'Font Awesome 5 Free'; font-family: 'Font Awesome 6 Free';
font-weight: 900; font-weight: 900;
content: '\f3ed'; content: '\f3ed';
color: $style-primary; color: $style-primary;
@ -114,7 +114,7 @@
&.host::after { &.host::after {
display: block; display: block;
font-family: 'Font Awesome 5 Free'; font-family: 'Font Awesome 6 Free';
font-weight: 900; font-weight: 900;
content: '\f521'; content: '\f521';
background: $style-primary; background: $style-primary;
@ -157,8 +157,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Ref, Vue } from 'vue-property-decorator'
import { Member } from '~/neko/types'
import Content from './context.vue' import Content from './context.vue'
import Avatar from './avatar.vue' import Avatar from './avatar.vue'

View File

@ -60,7 +60,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Vue } from 'vue-property-decorator'
import { messages } from '~/locale' import { messages } from '~/locale'
@Component({ name: 'neko-menu' }) @Component({ name: 'neko-menu' })

View File

@ -97,7 +97,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Ref, Vue } from 'vue-property-decorator'
import { ScreenResolution } from '~/neko/types' import { ScreenResolution } from '~/neko/types'
// @ts-ignore // @ts-ignore

View File

@ -304,7 +304,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Watch, Vue } from 'vue-property-decorator'
@Component({ name: 'neko-settings' }) @Component({ name: 'neko-settings' })
export default class extends Vue { export default class extends Vue {

View File

@ -6,6 +6,10 @@
<i class="fas fa-comment-alt" /> <i class="fas fa-comment-alt" />
<span>{{ $t('side.chat') }}</span> <span>{{ $t('side.chat') }}</span>
</li> </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')"> <li :class="{ active: tab === 'settings' }" @click.stop.prevent="change('settings')">
<i class="fas fa-sliders-h" /> <i class="fas fa-sliders-h" />
<span>{{ $t('side.settings') }}</span> <span>{{ $t('side.settings') }}</span>
@ -14,6 +18,7 @@
</div> </div>
<div class="page-container"> <div class="page-container">
<neko-chat v-if="tab === 'chat'" /> <neko-chat v-if="tab === 'chat'" />
<neko-files v-if="tab === 'files'" />
<neko-settings v-if="tab === 'settings'" /> <neko-settings v-if="tab === 'settings'" />
</div> </div>
</aside> </aside>
@ -74,23 +79,47 @@
</style> </style>
<script lang="ts"> <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 Settings from '~/components/settings.vue'
import Chat from '~/components/chat.vue' import Chat from '~/components/chat.vue'
import Files from '~/components/files.vue'
@Component({ @Component({
name: 'neko', name: 'neko',
components: { components: {
'neko-settings': Settings, 'neko-settings': Settings,
'neko-chat': Chat, 'neko-chat': Chat,
'neko-files': Files,
}, },
}) })
export default class extends Vue { export default class extends Vue {
get filetransferAllowed() {
return (
this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer'))
)
}
get tab() { get tab() {
return this.$accessor.client.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) { change(tab: string) {
this.$accessor.client.setTab(tab) this.$accessor.client.setTab(tab)
} }

View File

@ -68,7 +68,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Vue } from 'vue-property-decorator'
@Component({ name: 'neko-unsupported' }) @Component({ name: 'neko-unsupported' })
export default class extends Vue {} export default class extends Vue {}

View File

@ -8,10 +8,11 @@
<neko-emote :id="index" :key="index" /> <neko-emote :id="index" :key="index" />
</template> </template>
</div> </div>
<div <textarea
ref="overlay" ref="overlay"
class="overlay" class="overlay"
tabindex="0" tabindex="0"
data-gramm="false"
@click.stop.prevent @click.stop.prevent
@contextmenu.stop.prevent @contextmenu.stop.prevent
@wheel.stop.prevent="onWheel" @wheel.stop.prevent="onWheel"
@ -173,6 +174,12 @@
bottom: 0; bottom: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
cursor: default;
outline: 0;
border: 0;
color: transparent;
background: transparent;
resize: none;
} }
.player-aspect { .player-aspect {
@ -187,7 +194,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue, Prop } from 'vue-property-decorator' import { Component, Ref, Watch, Vue, Prop } from 'vue-property-decorator'
import ResizeObserver from 'resize-observer-polyfill' import ResizeObserver from 'resize-observer-polyfill'
import { elementRequestFullscreen, onFullscreenChange, isFullscreen } from '~/utils' import { elementRequestFullscreen, onFullscreenChange, isFullscreen, lockKeyboard, unlockKeyboard } from '~/utils'
import Emote from './emote.vue' import Emote from './emote.vue'
import Resolution from './resolution.vue' import Resolution from './resolution.vue'
@ -209,7 +216,7 @@
export default class extends Vue { export default class extends Vue {
@Ref('component') readonly _component!: HTMLElement @Ref('component') readonly _component!: HTMLElement
@Ref('container') readonly _container!: HTMLElement @Ref('container') readonly _container!: HTMLElement
@Ref('overlay') readonly _overlay!: HTMLElement @Ref('overlay') readonly _overlay!: HTMLTextAreaElement
@Ref('aspect') readonly _aspect!: HTMLElement @Ref('aspect') readonly _aspect!: HTMLElement
@Ref('player') readonly _player!: HTMLElement @Ref('player') readonly _player!: HTMLElement
@Ref('video') readonly _video!: HTMLVideoElement @Ref('video') readonly _video!: HTMLVideoElement
@ -332,12 +339,12 @@
} }
@Watch('width') @Watch('width')
onWidthChanged(width: number) { onWidthChanged() {
this.onResize() this.onResize()
} }
@Watch('height') @Watch('height')
onHeightChanged(height: number) { onHeightChanged() {
this.onResize() this.onResize()
} }
@ -410,6 +417,7 @@
onFullscreenChange(this._player, () => { onFullscreenChange(this._player, () => {
this.fullscreen = isFullscreen() this.fullscreen = isFullscreen()
this.fullscreen ? lockKeyboard() : unlockKeyboard()
this.onResize() this.onResize()
}) })
@ -436,7 +444,7 @@
this.$accessor.video.setPlayable(false) this.$accessor.video.setPlayable(false)
}) })
this._video.addEventListener('volumechange', (event) => { this._video.addEventListener('volumechange', () => {
this.$accessor.video.setMuted(this._video.muted) this.$accessor.video.setMuted(this._video.muted)
this.$accessor.video.setVolume(this._video.volume * 100) this.$accessor.video.setVolume(this._video.volume * 100)
}) })
@ -451,7 +459,7 @@
/* Initialize Guacamole Keyboard */ /* Initialize Guacamole Keyboard */
this.keyboard.onkeydown = (key: number) => { this.keyboard.onkeydown = (key: number) => {
if (!this.focused || !this.hosting || this.locked) { if (!this.hosting || this.locked) {
return true return true
} }
@ -459,7 +467,7 @@
return false return false
} }
this.keyboard.onkeyup = (key: number) => { this.keyboard.onkeyup = (key: number) => {
if (!this.focused || !this.hosting || this.locked) { if (!this.hosting || this.locked) {
return return
} }
@ -700,7 +708,6 @@
this.syncClipboard() this.syncClipboard()
} }
this._overlay.focus()
this.focused = true this.focused = true
} }
@ -724,5 +731,15 @@
this._container.style.maxWidth = `${(this.horizontal / this.vertical) * offsetHeight}px` this._container.style.maxWidth = `${(this.horizontal / this.vertical) * offsetHeight}px`
this._aspect.style.paddingBottom = `${(this.vertical / this.horizontal) * 100}%` this._aspect.style.paddingBottom = `${(this.vertical / this.horizontal) * 100}%`
} }
@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()
}
}
} }
</script> </script>

View File

@ -38,9 +38,9 @@ const exportMixin = {
$accessor() { $accessor() {
return neko return neko
}, },
$client () { $client() {
return window.$client return window.$client
} },
}, },
} }
@ -52,15 +52,8 @@ const plugini18n: PluginObject<undefined> = {
}, },
} }
function extend (component: any) { function extend(component: any) {
return component return component.use(plugini18n).use(Logger).use(Axios).use(Swal).use(Anime).use(Client).extend(exportMixin)
.use(plugini18n)
.use(Logger)
.use(Axios)
.use(Swal)
.use(Anime)
.use(Client)
.extend(exportMixin)
} }
export const NekoConnect = extend(Connect) export const NekoConnect = extend(Connect)

View File

@ -7,6 +7,7 @@ export const send_a_message = 'Sende eine Nachricht'
export const side = { export const side = {
chat: 'Chat', chat: 'Chat',
files: 'Dateien',
settings: 'Einstellungen', settings: 'Einstellungen',
} }
@ -68,6 +69,14 @@ export const locks = {
notif_locked: 'Raum gesperrt', notif_locked: 'Raum gesperrt',
notif_unlocked: 'Raum entsperrt', 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 = { export const setting = {
@ -108,3 +117,9 @@ export const notifications = {
muted: '{name} stummgeschaltet', muted: '{name} stummgeschaltet',
unmuted: '{name} stummschaltung aufgehoben', unmuted: '{name} stummschaltung aufgehoben',
} }
export const files = {
downloads: 'Herunterladen',
uploads: 'Hochladen',
upload_here: 'Klicken oder ziehen Sie Dateien zum Hochladen hierher',
}

View File

@ -7,6 +7,7 @@ export const send_a_message = 'Send a message'
export const side = { export const side = {
chat: 'Chat', chat: 'Chat',
files: 'Files',
settings: 'Settings', settings: 'Settings',
} }
@ -70,6 +71,14 @@ export const locks = {
notif_locked: 'locked the room', notif_locked: 'locked the room',
notif_unlocked: 'unlocked 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 = { export const setting = {
@ -110,3 +119,9 @@ export const notifications = {
muted: 'muted {name}', muted: 'muted {name}',
unmuted: 'unmuted {name}', unmuted: 'unmuted {name}',
} }
export const files = {
downloads: 'Downloads',
uploads: 'Uploads',
upload_here: 'Click or drag files here to upload',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Enviar un mensaje'
export const side = { export const side = {
chat: 'Chat', chat: 'Chat',
files: 'Archivos',
settings: 'Configuración', settings: 'Configuración',
} }
@ -74,6 +75,15 @@ export const locks = {
notif_locked: 'bloqueó la sala', notif_locked: 'bloqueó la sala',
notif_unlocked: 'desbloqueó 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 = { export const setting = {
@ -117,3 +127,9 @@ export const notifications = {
muted: '{name} silenciado', muted: '{name} silenciado',
unmuted: '{name} no silenciado', unmuted: '{name} no silenciado',
} }
export const files = {
downloads: 'Descargas',
uploads: 'Cargar',
upload_here: 'Haga clic o arrastre los archivos aquí para cargarlos',
}

View File

@ -7,6 +7,7 @@ export const send_a_message = 'Lähetä viesti'
export const side = { export const side = {
chat: 'Chatti', chat: 'Chatti',
files: 'Tiedostot',
settings: 'Asetukset', settings: 'Asetukset',
} }
@ -70,6 +71,15 @@ export const locks = {
notif_locked: 'lukittu huone', notif_locked: 'lukittu huone',
notif_unlocked: 'vapautettu 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 = { export const setting = {
@ -110,3 +120,9 @@ export const notifications = {
muted: 'mykistetty {name}', muted: 'mykistetty {name}',
unmuted: 'poistettu mykistys {name}', unmuted: 'poistettu mykistys {name}',
} }
export const files = {
downloads: 'Lataukset',
uploads: 'Lataa',
upload_here: 'Klikkaa tai vedä tiedostoja tähän ladataksesi',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Envoyer un message'
export const side = { export const side = {
chat: 'Chat', chat: 'Chat',
files: 'Fichiers',
settings: 'Paramètres', settings: 'Paramètres',
} }
@ -74,6 +75,15 @@ export const locks = {
notif_locked: 'a vérouillé la salle', notif_locked: 'a vérouillé la salle',
notif_unlocked: 'a dé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 = { export const setting = {
@ -117,3 +127,9 @@ export const notifications = {
muted: 'a mute {name}', muted: 'a mute {name}',
unmuted: 'a dé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',
}

View File

@ -8,6 +8,7 @@ import * as de from './de-de'
import * as ko from './ko-kr' import * as ko from './ko-kr'
import * as fi from './fi-fi' import * as fi from './fi-fi'
import * as ru from './ru-ru' import * as ru from './ru-ru'
import * as cn from './zh-cn'
export const messages = { export const messages = {
en, en,
@ -20,4 +21,5 @@ export const messages = {
ko, ko,
fi, fi,
ru, ru,
cn,
} }

View File

@ -7,6 +7,7 @@ export const send_a_message = '메세지 보내기'
export const side = { export const side = {
chat: '채팅', chat: '채팅',
files: '파일',
settings: '설정', settings: '설정',
} }
@ -68,6 +69,15 @@ export const locks = {
notif_locked: '방이 잠겼습니다', notif_locked: '방이 잠겼습니다',
notif_unlocked: '방 잠금이 해제됐습니다', 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 = { export const setting = {
@ -108,3 +118,9 @@ export const notifications = {
muted: '{name} 님이 뮤트됐습니다', muted: '{name} 님이 뮤트됐습니다',
unmuted: '{name} 님의 뮤트가 해제됐습니다', unmuted: '{name} 님의 뮤트가 해제됐습니다',
} }
export const files = {
downloads: '다운로드',
uploads: '업로드',
upload_here: '업로드할 파일을 여기로 클릭하거나 드래그하세요.',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Send en melding'
export const side = { export const side = {
chat: 'Sludring', chat: 'Sludring',
files: 'Filer',
settings: 'Innstillinger', settings: 'Innstillinger',
} }
@ -74,6 +75,15 @@ export const locks = {
notif_locked: 'låste rommet', notif_locked: 'låste rommet',
notif_unlocked: 'låste opp 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 = { export const setting = {
@ -117,3 +127,9 @@ export const notifications = {
muted: 'forstummet {name}', muted: 'forstummet {name}',
unmuted: 'opphevet forstummingen av {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',
}

View File

@ -7,6 +7,7 @@ export const send_a_message = 'Отправить сообщение'
export const side = { export const side = {
chat: 'Чат', chat: 'Чат',
files: 'Файлы',
settings: 'Настройки', settings: 'Настройки',
} }
@ -70,6 +71,15 @@ export const locks = {
notif_locked: 'комната закрыта', notif_locked: 'комната закрыта',
notif_unlocked: 'комната открыта', 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 = { export const setting = {
@ -110,3 +120,9 @@ export const notifications = {
muted: 'заглушен {name}', muted: 'заглушен {name}',
unmuted: 'не заглушен {name}', unmuted: 'не заглушен {name}',
} }
export const files = {
downloads: 'Загрузки',
uploads: 'Загрузить',
upload_here: 'Нажмите или перетащите сюда файлы для загрузки',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Odoslať správu'
export const side = { export const side = {
chat: 'Chat', chat: 'Chat',
files: 'Súbory',
settings: 'Nastavenia', settings: 'Nastavenia',
} }
@ -73,6 +74,15 @@ export const locks = {
notif_locked: 'miestnosť bola zamknutá', notif_locked: 'miestnosť bola zamknutá',
notif_unlocked: 'miestnosť bola odomknutá', 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 = { export const setting = {
@ -113,3 +123,9 @@ export const notifications = {
muted: 'zakázal chat používateľovi {name}', muted: 'zakázal chat používateľovi {name}',
unmuted: 'povolil 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ť',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Skicka ett meddelande'
export const side = { export const side = {
chat: 'Chatt', chat: 'Chatt',
files: 'Filer',
settings: 'Inställningar', settings: 'Inställningar',
} }
@ -74,6 +75,15 @@ export const locks = {
notif_locked: 'låste rummet', notif_locked: 'låste rummet',
notif_unlocked: 'låste upp 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 = { export const setting = {
@ -117,3 +127,9 @@ export const notifications = {
muted: 'tystade {name}', muted: 'tystade {name}',
unmuted: 'tog bort tystningen på {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
View 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: '点击或拖动文件到这里来上传',
}

View File

@ -66,8 +66,8 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this._ws = new WebSocket(`${url}?password=${encodeURIComponent(password)}`) this._ws = new WebSocket(`${url}?password=${encodeURIComponent(password)}`)
this.emit('debug', `connecting to ${this._ws.url}`) this.emit('debug', `connecting to ${this._ws.url}`)
this._ws.onmessage = this.onMessage.bind(this) this._ws.onmessage = this.onMessage.bind(this)
this._ws.onerror = (event) => this.onError.bind(this) this._ws.onerror = () => this.onError.bind(this)
this._ws.onclose = (event) => this.onDisconnected.bind(this, new Error('websocket closed')) this._ws.onclose = () => this.onDisconnected.bind(this, new Error('websocket closed'))
this._timeout = window.setTimeout(this.onTimeout.bind(this), 15000) this._timeout = window.setTimeout(this.onTimeout.bind(this), 15000)
} catch (err: any) { } catch (err: any) {
this.onDisconnected(err) this.onDisconnected(err)
@ -210,15 +210,15 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
}) })
} }
this._peer.onconnectionstatechange = (event) => { this._peer.onconnectionstatechange = () => {
this.emit('debug', `peer connection state changed`, this._peer ? this._peer.connectionState : undefined) this.emit('debug', `peer connection state changed`, this._peer ? this._peer.connectionState : undefined)
} }
this._peer.onsignalingstatechange = (event) => { this._peer.onsignalingstatechange = () => {
this.emit('debug', `peer signaling state changed`, this._peer ? this._peer.signalingState : undefined) this.emit('debug', `peer signaling state changed`, this._peer ? this._peer.signalingState : undefined)
} }
this._peer.oniceconnectionstatechange = (event) => { this._peer.oniceconnectionstatechange = () => {
this._state = this._peer!.iceConnectionState this._state = this._peer!.iceConnectionState
this.emit('debug', `peer ice connection state changed: ${this._peer!.iceConnectionState}`) this.emit('debug', `peer ice connection state changed: ${this._peer!.iceConnectionState}`)
@ -286,6 +286,10 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
try { try {
const d = await this._peer.createAnswer() const d = await this._peer.createAnswer()
// add stereo=1 to answer sdp to enable stereo audio for chromium
d.sdp = d.sdp?.replace(/(stereo=1;)?useinbandfec=1/, 'useinbandfec=1;stereo=1')
this._peer!.setLocalDescription(d) this._peer!.setLocalDescription(d)
this._ws!.send( this._ws!.send(
@ -309,6 +313,22 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this._peer.setRemoteDescription({ type: 'answer', sdp }) this._peer.setRemoteDescription({ type: 'answer', sdp })
} }
public addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender {
if (!this._peer) {
throw new Error('peer not connected')
}
return this._peer.addTrack(track, ...streams)
}
public removeTrack(sender: RTCRtpSender) {
if (!this._peer) {
throw new Error('peer not connected')
}
this._peer.removeTrack(sender)
}
private async onMessage(e: MessageEvent) { private async onMessage(e: MessageEvent) {
const { event, ...payload } = JSON.parse(e.data) as WebSocketMessages const { event, ...payload } = JSON.parse(e.data) as WebSocketMessages

View File

@ -38,6 +38,10 @@ export const EVENT = {
MESSAGE: 'chat/message', MESSAGE: 'chat/message',
EMOTE: 'chat/emote', EMOTE: 'chat/emote',
}, },
FILETRANSFER: {
LIST: 'filetransfer/list',
REFRESH: 'filetransfer/refresh',
},
SCREEN: { SCREEN: {
CONFIGURATIONS: 'screen/configurations', CONFIGURATIONS: 'screen/configurations',
RESOLUTION: 'screen/resolution', RESOLUTION: 'screen/resolution',
@ -69,6 +73,7 @@ export type WebSocketEvents =
| MemberEvents | MemberEvents
| SignalEvents | SignalEvents
| ChatEvents | ChatEvents
| FileTransferEvents
| ScreenEvents | ScreenEvents
| BroadcastEvents | BroadcastEvents
| AdminEvents | AdminEvents
@ -91,6 +96,9 @@ export type SignalEvents =
| typeof EVENT.SIGNAL.CANDIDATE | typeof EVENT.SIGNAL.CANDIDATE
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE 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 ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET
export type BroadcastEvents = export type BroadcastEvents =

View File

@ -7,7 +7,6 @@ import { accessor } from '~/store'
import { import {
SystemMessagePayload, SystemMessagePayload,
SignalProvidePayload,
MemberListPayload, MemberListPayload,
MemberDisconnectPayload, MemberDisconnectPayload,
MemberPayload, MemberPayload,
@ -19,11 +18,11 @@ import {
ScreenConfigurationsPayload, ScreenConfigurationsPayload,
ScreenResolutionPayload, ScreenResolutionPayload,
BroadcastStatusPayload, BroadcastStatusPayload,
AdminPayload,
AdminTargetPayload, AdminTargetPayload,
AdminLockMessage, AdminLockMessage,
SystemInitPayload, SystemInitPayload,
AdminLockResource, AdminLockResource,
FileTransferListPayload,
} from './messages' } from './messages'
interface NekoEvents extends BaseEvents {} interface NekoEvents extends BaseEvents {}
@ -46,6 +45,8 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
this.$vue = vue this.$vue = vue
this.$accessor = vue.$accessor this.$accessor = vue.$accessor
this.url = url this.url = url
// convert ws url to http url
this.$vue.$http.defaults.baseURL = url.replace(/^ws/, 'http').replace(/\/ws$/, '')
} }
private cleanup() { private cleanup() {
@ -128,13 +129,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
this.$accessor.video.setStream(0) this.$accessor.video.setStream(0)
} }
protected [EVENT.DATA](data: any) {} protected [EVENT.DATA]() {}
///////////////////////////// /////////////////////////////
// System Events // System Events
///////////////////////////// /////////////////////////////
protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks }: SystemInitPayload) { protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks, file_transfer }: SystemInitPayload) {
this.$accessor.remote.setImplicitHosting(implicit_hosting) this.$accessor.remote.setImplicitHosting(implicit_hosting)
this.$accessor.remote.setFileTransfer(file_transfer)
for (const resource in locks) { for (const resource in locks) {
this[EVENT.ADMIN.LOCK]({ this[EVENT.ADMIN.LOCK]({
@ -351,6 +353,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
this.$accessor.chat.newEmote({ type: emote }) 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 // Screen Events
///////////////////////////// /////////////////////////////

View File

@ -8,8 +8,9 @@ import {
ChatEvents, ChatEvents,
ScreenEvents, ScreenEvents,
AdminEvents, AdminEvents,
FileTransferEvents,
} from './events' } from './events'
import { Member, ScreenConfigurations, ScreenResolution } from './types' import { FileListItem, Member, ScreenConfigurations, ScreenResolution } from './types'
export type WebSocketMessages = export type WebSocketMessages =
| WebSocketMessage | WebSocketMessage
@ -59,6 +60,7 @@ export interface SystemInit extends WebSocketMessage, SystemInitPayload {
export interface SystemInitPayload { export interface SystemInitPayload {
implicit_hosting: boolean implicit_hosting: boolean
locks: Record<string, string> locks: Record<string, string>
file_transfer: boolean
} }
// system/disconnect // system/disconnect
@ -192,6 +194,18 @@ export interface EmojiSendPayload {
emote: string emote: string
} }
/*
FILE TRANSFER PAYLOADS
*/
export interface FileTransferListMessage extends WebSocketMessage, FileTransferListPayload {
event: FileTransferEvents
}
export interface FileTransferListPayload {
cwd: string
files: FileListItem[]
}
/* /*
SCREEN PAYLOADS SCREEN PAYLOADS
*/ */
@ -215,11 +229,11 @@ export interface ScreenConfigurationsPayload {
BROADCAST PAYLOADS BROADCAST PAYLOADS
*/ */
export interface BroadcastCreatePayload { export interface BroadcastCreatePayload {
url: string url: string
} }
export interface BroadcastStatusPayload { export interface BroadcastStatusPayload {
url: string url: string
isActive: boolean isActive: boolean
} }
@ -248,7 +262,7 @@ export interface AdminLockMessage extends WebSocketMessage, AdminLockPayload {
id: string id: string
} }
export type AdminLockResource = 'login' | 'control' export type AdminLockResource = 'login' | 'control' | 'file_transfer'
export interface AdminLockPayload { export interface AdminLockPayload {
resource: AdminLockResource resource: AdminLockResource

View File

@ -22,3 +22,20 @@ export interface ScreenResolution {
height: number height: number
rate: 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
}

View File

@ -10,7 +10,7 @@ declare module 'vue/types/vue' {
$swal: VueSwalInstance $swal: VueSwalInstance
} }
interface VueConstructor<V extends Vue = Vue> { interface VueConstructor {
swal: VueSwalInstance swal: VueSwalInstance
} }
} }

View File

@ -73,7 +73,7 @@ export const actions = actionTree(
accessor.chat.addEmote({ id, emote }) accessor.chat.addEmote({ id, emote })
}, },
newMessage({ state }, message: Message) { newMessage(store, message: Message) {
if (accessor.settings.chat_sound) { if (accessor.settings.chat_sound) {
new Audio('chat.mp3').play().catch(console.error) new Audio('chat.mp3').play().catch(console.error)
} }

72
client/src/store/files.ts Normal file
View 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() {
for (const t of accessor.files.transfers) {
if (t.status !== 'completed') {
t.abortController?.abort()
}
accessor.files.removeTransfer(t)
}
},
refresh() {
if (!accessor.connected) {
return
}
$client.sendMessage(EVENT.FILETRANSFER.REFRESH)
},
},
)

View File

@ -1,12 +1,13 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' 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 { EVENT } from '~/neko/events'
import { AdminLockResource } from '~/neko/messages' import { AdminLockResource } from '~/neko/messages'
import { get, set } from '~/utils/localstorage' import { get, set } from '~/utils/localstorage'
import * as video from './video' import * as video from './video'
import * as chat from './chat' import * as chat from './chat'
import * as files from './files'
import * as remote from './remote' import * as remote from './remote'
import * as user from './user' import * as user from './user'
import * as settings from './settings' import * as settings from './settings'
@ -55,10 +56,14 @@ 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( export const actions = actionTree(
{ state, mutations }, { state, getters, mutations },
{ {
initialise(store) { initialise() {
accessor.emoji.initialise() accessor.emoji.initialise()
accessor.settings.initialise() accessor.settings.initialise()
}, },
@ -79,12 +84,20 @@ export const actions = actionTree(
$client.sendMessage(EVENT.ADMIN.UNLOCK, { resource }) $client.sendMessage(EVENT.ADMIN.UNLOCK, { resource })
}, },
login({ state }, { displayname, password }: { displayname: string; password: string }) { toggleLock(_, resource: AdminLockResource) {
if (accessor.isLocked(resource)) {
accessor.unlock(resource)
} else {
accessor.lock(resource)
}
},
login(store, { displayname, password }: { displayname: string; password: string }) {
accessor.setLogin({ displayname, password }) accessor.setLogin({ displayname, password })
$client.login(password, displayname) $client.login(password, displayname)
}, },
logout({ state }) { logout() {
accessor.setLogin({ displayname: '', password: '' }) accessor.setLogin({ displayname: '', password: '' })
set('displayname', '') set('displayname', '')
set('password', '') set('password', '')
@ -97,7 +110,8 @@ export const storePattern = {
state, state,
mutations, mutations,
actions, actions,
modules: { video, chat, user, remote, settings, client, emoji }, getters,
modules: { video, chat, files, user, remote, settings, client, emoji },
} }
Vue.use(Vuex) Vue.use(Vuex)

View File

@ -13,6 +13,7 @@ export const state = () => ({
clipboard: '', clipboard: '',
locked: false, locked: false,
implicitHosting: true, implicitHosting: true,
fileTransfer: true,
keyboardModifierState: -1, keyboardModifierState: -1,
}) })
@ -20,7 +21,7 @@ export const getters = getterTree(state, {
hosting: (state, getters, root) => { hosting: (state, getters, root) => {
return root.user.id === state.id || state.implicitHosting return root.user.id === state.id || state.implicitHosting
}, },
hosted: (state, getters, root) => { hosted: (state) => {
return state.id !== '' || state.implicitHosting return state.id !== '' || state.implicitHosting
}, },
host: (state, getters, root) => { host: (state, getters, root) => {
@ -53,6 +54,10 @@ export const mutations = mutationTree(state, {
state.implicitHosting = val state.implicitHosting = val
}, },
setFileTransfer(state, val: boolean) {
state.fileTransfer = val
},
reset(state) { reset(state) {
state.id = '' state.id = ''
state.clipboard = '' state.clipboard = ''
@ -131,7 +136,7 @@ export const actions = actionTree(
$client.sendMessage(EVENT.ADMIN.RELEASE) $client.sendMessage(EVENT.ADMIN.RELEASE)
}, },
adminGive({ getters }, member: string | Member) { adminGive(store, member: string | Member) {
if (!accessor.connected) { if (!accessor.connected) {
return return
} }
@ -155,7 +160,7 @@ export const actions = actionTree(
$client.sendMessage(EVENT.CONTROL.KEYBOARD, { layout: accessor.settings.keyboard_layout }) $client.sendMessage(EVENT.CONTROL.KEYBOARD, { layout: accessor.settings.keyboard_layout })
}, },
syncKeyboardModifierState({ state, getters }, { capsLock, numLock, scrollLock }) { syncKeyboardModifierState({ state }, { capsLock, numLock, scrollLock }) {
if (state.keyboardModifierState === keyboardModifierState(capsLock, numLock, scrollLock)) { if (state.keyboardModifierState === keyboardModifierState(capsLock, numLock, scrollLock)) {
return return
} }

View File

@ -79,13 +79,13 @@ export const actions = actionTree(
} }
}, },
broadcastStatus({ getters }, { url, isActive }) { broadcastStatus(store, { url, isActive }) {
accessor.settings.setBroadcastStatus({ url, isActive }) accessor.settings.setBroadcastStatus({ url, isActive })
}, },
broadcastCreate({ getters }, url: string) { broadcastCreate(store, url: string) {
$client.sendMessage(EVENT.BROADCAST.CREATE, { url }) $client.sendMessage(EVENT.BROADCAST.CREATE, { url })
}, },
broadcastDestroy({ getters }) { broadcastDestroy() {
$client.sendMessage(EVENT.BROADCAST.DESTROY) $client.sendMessage(EVENT.BROADCAST.DESTROY)
}, },
}, },

View File

@ -169,7 +169,7 @@ export const mutations = mutationTree(state, {
export const actions = actionTree( export const actions = actionTree(
{ state, getters, mutations }, { state, getters, mutations },
{ {
screenConfiguations({ state }) { screenConfiguations() {
if (!accessor.connected || !accessor.user.admin) { if (!accessor.connected || !accessor.user.admin) {
return return
} }
@ -177,7 +177,7 @@ export const actions = actionTree(
$client.sendMessage(EVENT.SCREEN.CONFIGURATIONS) $client.sendMessage(EVENT.SCREEN.CONFIGURATIONS)
}, },
screenGet({ state }) { screenGet() {
if (!accessor.connected) { if (!accessor.connected) {
return return
} }
@ -185,7 +185,7 @@ export const actions = actionTree(
$client.sendMessage(EVENT.SCREEN.RESOLUTION) $client.sendMessage(EVENT.SCREEN.RESOLUTION)
}, },
screenSet({ state }, resolution: ScreenResolution) { screenSet(store, resolution: ScreenResolution) {
if (!accessor.connected || !accessor.user.admin) { if (!accessor.connected || !accessor.user.admin) {
return return
} }

View File

@ -0,0 +1,15 @@
// navigator.keyboard.d.ts
// Type declarations for Keyboard API
// https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API
interface Keyboard {
lock(keyCodes?: array<string>): Promise<void>
unlock(): void
}
interface NavigatorKeyboard {
// Only available in a secure context.
readonly keyboard?: Keyboard
}
interface Navigator extends NavigatorKeyboard {}

View File

@ -1,4 +1,5 @@
/* eslint-disable */ /* eslint-disable */
/* /*
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
@ -35,7 +36,9 @@ Guacamole.Keyboard = function Keyboard(element) {
/** /**
* Reference to this Guacamole.Keyboard. * Reference to this Guacamole.Keyboard.
*
* @private * @private
* @type {!Guacamole.Keyboard}
*/ */
var guac_keyboard = this; var guac_keyboard = this;
@ -44,7 +47,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* instance with respect to other Guacamole.Keyboard instances. * instance with respect to other Guacamole.Keyboard instances.
* *
* @private * @private
* @type {Number} * @type {!number}
*/ */
var guacKeyboardID = Guacamole.Keyboard._nextID++; var guacKeyboardID = Guacamole.Keyboard._nextID++;
@ -54,7 +57,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* *
* @private * @private
* @constant * @constant
* @type {String} * @type {!string}
*/ */
var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID; var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID;
@ -63,9 +66,12 @@ Guacamole.Keyboard = function Keyboard(element) {
* with this Guacamole.Keyboard in focus. * with this Guacamole.Keyboard in focus.
* *
* @event * @event
* @param {Number} keysym The keysym of the key being pressed. * @param {!number} keysym
* @return {Boolean} true if the key event should be allowed through to the * The keysym of the key being pressed.
* browser, false otherwise. *
* @return {!boolean}
* true if the key event should be allowed through to the browser,
* false otherwise.
*/ */
this.onkeydown = null; this.onkeydown = null;
@ -74,7 +80,8 @@ Guacamole.Keyboard = function Keyboard(element) {
* with this Guacamole.Keyboard in focus. * with this Guacamole.Keyboard in focus.
* *
* @event * @event
* @param {Number} keysym The keysym of the key being released. * @param {!number} keysym
* The keysym of the key being released.
*/ */
this.onkeyup = null; this.onkeyup = null;
@ -84,14 +91,14 @@ Guacamole.Keyboard = function Keyboard(element) {
* reliably detect that quirk is to platform/browser-sniff. * reliably detect that quirk is to platform/browser-sniff.
* *
* @private * @private
* @type {Object.<String, Boolean>} * @type {!Object.<string, boolean>}
*/ */
var quirks = { var quirks = {
/** /**
* Whether keyup events are universally unreliable. * Whether keyup events are universally unreliable.
* *
* @type {Boolean} * @type {!boolean}
*/ */
keyupUnreliable: false, keyupUnreliable: false,
@ -99,7 +106,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* Whether the Alt key is actually a modifier for typable keys and is * Whether the Alt key is actually a modifier for typable keys and is
* thus never used for keyboard shortcuts. * thus never used for keyboard shortcuts.
* *
* @type {Boolean} * @type {!boolean}
*/ */
altIsTypableOnly: false, altIsTypableOnly: false,
@ -107,7 +114,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* Whether we can rely on receiving a keyup event for the Caps Lock * Whether we can rely on receiving a keyup event for the Caps Lock
* key. * key.
* *
* @type {Boolean} * @type {!boolean}
*/ */
capsLockKeyupUnreliable: false capsLockKeyupUnreliable: false
@ -137,26 +144,75 @@ Guacamole.Keyboard = function Keyboard(element) {
* *
* @private * @private
* @constructor * @constructor
* @param {KeyboardEvent} [orig]
* The relevant DOM keyboard event.
*/ */
var KeyEvent = function() { var KeyEvent = function KeyEvent(orig) {
/** /**
* Reference to this key event. * Reference to this key event.
*
* @private
* @type {!KeyEvent}
*/ */
var key_event = this; 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 * An arbitrary timestamp in milliseconds, indicating this event's
* position in time relative to other events. * position in time relative to other events.
* *
* @type {Number} * @type {!number}
*/ */
this.timestamp = new Date().getTime(); this.timestamp = new Date().getTime();
/** /**
* Whether the default action of this key event should be prevented. * Whether the default action of this key event should be prevented.
* *
* @type {Boolean} * @type {!boolean}
*/ */
this.defaultPrevented = false; this.defaultPrevented = false;
@ -165,7 +221,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* by a best-effort guess using available event properties and keyboard * by a best-effort guess using available event properties and keyboard
* state. * state.
* *
* @type {Number} * @type {number}
*/ */
this.keysym = null; 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, * 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. * and future key events may be a better source of information.
* *
* @type {Boolean} * @type {!boolean}
*/ */
this.reliable = false; this.reliable = false;
@ -182,8 +238,9 @@ Guacamole.Keyboard = function Keyboard(element) {
* Returns the number of milliseconds elapsed since this event was * Returns the number of milliseconds elapsed since this event was
* received. * received.
* *
* @return {Number} The number of milliseconds elapsed since this * @return {!number}
* event was received. * The number of milliseconds elapsed since this event was
* received.
*/ */
this.getAge = function() { this.getAge = function() {
return new Date().getTime() - key_event.timestamp; return new Date().getTime() - key_event.timestamp;
@ -199,62 +256,23 @@ Guacamole.Keyboard = function Keyboard(element) {
* @private * @private
* @constructor * @constructor
* @augments Guacamole.Keyboard.KeyEvent * @augments Guacamole.Keyboard.KeyEvent
* @param {Number} keyCode The JavaScript key code of the key pressed. * @param {!KeyboardEvent} orig
* @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key * The relevant DOM "keydown" event.
* 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
*/ */
var KeydownEvent = function(keyCode, keyIdentifier, key, location) { var KeydownEvent = function KeydownEvent(orig) {
// We extend KeyEvent // We extend KeyEvent
KeyEvent.apply(this); KeyEvent.call(this, orig);
/**
* 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;
// If key is known from keyCode or DOM3 alone, use that // If key is known from keyCode or DOM3 alone, use that
this.keysym = keysym_from_key_identifier(key, location) this.keysym = keysym_from_key_identifier(this.key, this.location)
|| keysym_from_keycode(keyCode, location); || keysym_from_keycode(this.keyCode, this.location);
/** /**
* Whether the keyup following this keydown event is known to be * Whether the keyup following this keydown event is known to be
* reliable. If false, we cannot rely on the keyup event to occur. * reliable. If false, we cannot rely on the keyup event to occur.
* *
* @type {Boolean} * @type {!boolean}
*/ */
this.keyupReliable = !quirks.keyupUnreliable; this.keyupReliable = !quirks.keyupUnreliable;
@ -264,12 +282,12 @@ Guacamole.Keyboard = function Keyboard(element) {
this.reliable = true; this.reliable = true;
// Use legacy keyIdentifier as a last resort, if it looks sane // Use legacy keyIdentifier as a last resort, if it looks sane
if (!this.keysym && key_identifier_sane(keyCode, keyIdentifier)) if (!this.keysym && key_identifier_sane(this.keyCode, this.keyIdentifier))
this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift); 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 // If a key is pressed while meta is held down, the keyup will
// never be sent in Chrome (bug #108404) // 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; this.keyupReliable = false;
// We cannot rely on receiving keyup for Caps Lock on certain platforms // We cannot rely on receiving keyup for Caps Lock on certain platforms
@ -277,21 +295,21 @@ Guacamole.Keyboard = function Keyboard(element) {
this.keyupReliable = false; this.keyupReliable = false;
// Determine whether default action for Alt+combinations must be prevented // 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 // 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 // We must rely on the (potentially buggy) keyIdentifier if preventing
// the default action is important // the default action is important
if ((prevent_ctrl && guac_keyboard.modifiers.ctrl) if ((prevent_ctrl && this.modifiers.ctrl)
|| (prevent_alt && guac_keyboard.modifiers.alt) || (prevent_alt && this.modifiers.alt)
|| guac_keyboard.modifiers.meta || this.modifiers.meta
|| guac_keyboard.modifiers.hyper) || this.modifiers.hyper)
this.reliable = true; this.reliable = true;
// Record most recently known keysym by associated key code // 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 * @private
* @constructor * @constructor
* @augments Guacamole.Keyboard.KeyEvent * @augments Guacamole.Keyboard.KeyEvent
* @param {Number} charCode The Unicode codepoint of the character that * @param {!KeyboardEvent} orig
* would be typed by the key pressed. * The relevant DOM "keypress" event.
*/ */
var KeypressEvent = function(charCode) { var KeypressEvent = function KeypressEvent(orig) {
// We extend KeyEvent // We extend KeyEvent
KeyEvent.apply(this); KeyEvent.call(this, orig);
/**
* The Unicode codepoint of the character that would be typed by the
* key pressed.
*
* @type {Number}
*/
this.charCode = charCode;
// Pull keysym from char code // Pull keysym from char code
this.keysym = keysym_from_charcode(charCode); this.keysym = keysym_from_charcode(this.keyCode);
// Keypress is always reliable // Keypress is always reliable
this.reliable = true; this.reliable = true;
@ -332,68 +342,30 @@ Guacamole.Keyboard = function Keyboard(element) {
KeypressEvent.prototype = new KeyEvent(); 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 * associated with a printable character. The presence or absence of any
* information within this object is browser-dependent. * information within this object is browser-dependent.
* *
* @private * @private
* @constructor * @constructor
* @augments Guacamole.Keyboard.KeyEvent * @augments Guacamole.Keyboard.KeyEvent
* @param {Number} keyCode The JavaScript key code of the key released. * @param {!KeyboardEvent} orig
* @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key * The relevant DOM "keyup" event.
* 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
*/ */
var KeyupEvent = function(keyCode, keyIdentifier, key, location) { var KeyupEvent = function KeyupEvent(orig) {
// We extend KeyEvent // We extend KeyEvent
KeyEvent.apply(this); KeyEvent.call(this, orig);
/** // If key is known from keyCode or DOM3 alone, use that (keyCode is
* The JavaScript key code of the key released. // still more reliable for keyup when dead keys are in use)
* this.keysym = keysym_from_keycode(this.keyCode, this.location)
* @type {Number} || keysym_from_key_identifier(this.key, this.location);
*/
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
// Fall back to the most recently pressed keysym associated with the // Fall back to the most recently pressed keysym associated with the
// keyCode if the inferred key doesn't seem to actually be pressed // keyCode if the inferred key doesn't seem to actually be pressed
if (!guac_keyboard.pressed[this.keysym]) 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 // Keyup is as reliable as it will ever be
this.reliable = true; this.reliable = true;
@ -407,14 +379,16 @@ Guacamole.Keyboard = function Keyboard(element) {
* KeydownEvent, KeypressEvent, and KeyupEvent classes. * KeydownEvent, KeypressEvent, and KeyupEvent classes.
* *
* @private * @private
* @type {KeyEvent[]} * @type {!KeyEvent[]}
*/ */
var eventLog = []; var eventLog = [];
/** /**
* Map of known JavaScript keycodes which do not map to typable characters * Map of known JavaScript keycodes which do not map to typable characters
* to their X11 keysym equivalents. * to their X11 keysym equivalents.
*
* @private * @private
* @type {!Object.<number, number[]>}
*/ */
var keycodeKeysyms = { var keycodeKeysyms = {
8: [0xFF08], // backspace 8: [0xFF08], // backspace
@ -438,9 +412,9 @@ Guacamole.Keyboard = function Keyboard(element) {
40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2 40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2
45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0 45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0
46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal 46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal
91: [0xFFEB], // left window key (hyper_l) 91: [0xFFE7], // left windows/command key (meta_l)
92: [0xFF67], // right window key (menu key?) 92: [0xFFE8], // right window/command key (meta_r)
93: null, // select key 93: [0xFF67], // menu key
96: [0xFFB0], // KP 0 96: [0xFFB0], // KP 0
97: [0xFFB1], // KP 1 97: [0xFFB1], // KP 1
98: [0xFFB2], // KP 2 98: [0xFFB2], // KP 2
@ -476,7 +450,9 @@ Guacamole.Keyboard = function Keyboard(element) {
/** /**
* Map of known JavaScript keyidentifiers which do not map to typable * Map of known JavaScript keyidentifiers which do not map to typable
* characters to their unshifted X11 keysym equivalents. * characters to their unshifted X11 keysym equivalents.
*
* @private * @private
* @type {!Object.<string, number[]>}
*/ */
var keyidentifier_keysym = { var keyidentifier_keysym = {
"Again": [0xFF66], "Again": [0xFF66],
@ -584,14 +560,16 @@ Guacamole.Keyboard = function Keyboard(element) {
"UIKeyInputUpArrow": [0xFF52], "UIKeyInputUpArrow": [0xFF52],
"Up": [0xFF52], "Up": [0xFF52],
"Undo": [0xFF65], "Undo": [0xFF65],
"Win": [0xFFEB], "Win": [0xFFE7, 0xFFE7, 0xFFE8],
"Zenkaku": [0xFF28], "Zenkaku": [0xFF28],
"ZenkakuHankaku": [0xFF2A] "ZenkakuHankaku": [0xFF2A]
}; };
/** /**
* All keysyms which should not repeat when held down. * All keysyms which should not repeat when held down.
*
* @private * @private
* @type {!Object.<number, boolean>}
*/ */
var no_repeat = { var no_repeat = {
0xFE03: true, // ISO Level 3 Shift (AltGr) 0xFE03: true, // ISO Level 3 Shift (AltGr)
@ -604,12 +582,14 @@ Guacamole.Keyboard = function Keyboard(element) {
0xFFE8: true, // Right meta 0xFFE8: true, // Right meta
0xFFE9: true, // Left alt 0xFFE9: true, // Left alt
0xFFEA: true, // Right alt 0xFFEA: true, // Right alt
0xFFEB: true, // Left hyper 0xFFEB: true, // Left super/hyper
0xFFEC: true // Right hyper 0xFFEC: true // Right super/hyper
}; };
/** /**
* All modifiers and their states. * All modifiers and their states.
*
* @type {!Guacamole.Keyboard.ModifierState}
*/ */
this.modifiers = new 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 * 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 * pressed, the value of pressed for that keysym will be true. If a key
* is not currently pressed, it will not be defined. * is not currently pressed, it will not be defined.
*
* @type {!Object.<number, boolean>}
*/ */
this.pressed = {}; this.pressed = {};
@ -629,7 +611,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* of the key is explicitly known), it will not be defined. * of the key is explicitly known), it will not be defined.
* *
* @private * @private
* @tyle {Object.<Number, Boolean>} * @type {!Object.<number, boolean>}
*/ */
var implicitlyPressed = {}; var implicitlyPressed = {};
@ -640,6 +622,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* is (theoretically) still pressed. * is (theoretically) still pressed.
* *
* @private * @private
* @type {!Object.<number, boolean>}
*/ */
var last_keydown_result = {}; var last_keydown_result = {};
@ -648,20 +631,24 @@ Guacamole.Keyboard = function Keyboard(element) {
* fired. This object maps keycodes to keysyms. * fired. This object maps keycodes to keysyms.
* *
* @private * @private
* @type {Object.<Number, Number>} * @type {!Object.<number, number>}
*/ */
var recentKeysym = {}; var recentKeysym = {};
/** /**
* Timeout before key repeat starts. * Timeout before key repeat starts.
*
* @private * @private
* @type {number}
*/ */
var key_repeat_timeout = null; var key_repeat_timeout = null;
/** /**
* Interval which presses and releases the last key pressed while that * Interval which presses and releases the last key pressed while that
* key is still being held down. * key is still being held down.
*
* @private * @private
* @type {number}
*/ */
var key_repeat_interval = null; var key_repeat_interval = null;
@ -671,11 +658,11 @@ Guacamole.Keyboard = function Keyboard(element) {
* undefined. * undefined.
* *
* @private * @private
* @param {Number[]} keysyms * @param {number[]} keysyms
* An array of keysyms, where the index of the keysym in the array is * An array of keysyms, where the index of the keysym in the array is
* the location value. * the location value.
* *
* @param {Number} location * @param {!number} location
* The location on the keyboard corresponding to the key pressed, as * The location on the keyboard corresponding to the key pressed, as
* defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent * 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, * Returns true if the given keysym corresponds to a printable character,
* false otherwise. * false otherwise.
* *
* @param {Number} keysym * @param {!number} keysym
* The keysym to check. * The keysym to check.
* *
* @returns {Boolean} * @returns {!boolean}
* true if the given keysym corresponds to a printable character, * true if the given keysym corresponds to a printable character,
* false otherwise. * false otherwise.
*/ */
@ -773,13 +760,13 @@ Guacamole.Keyboard = function Keyboard(element) {
* correct in all cases. * correct in all cases.
* *
* @private * @private
* @param {Number} keyCode * @param {!number} keyCode
* The keyCode from a browser keydown/keyup event. * The keyCode from a browser keydown/keyup event.
* *
* @param {String} keyIdentifier * @param {string} keyIdentifier
* The legacy keyIdentifier from a browser keydown/keyup event. * The legacy keyIdentifier from a browser keydown/keyup event.
* *
* @returns {Boolean} * @returns {!boolean}
* true if the keyIdentifier looks sane, false if the keyIdentifier * true if the keyIdentifier looks sane, false if the keyIdentifier
* appears incorrectly derived or is missing entirely. * 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 * not a modifier. The return value of this function depends on the
* return value of the keydown event handler, if any. * return value of the keydown event handler, if any.
* *
* @param {Number} keysym The keysym of the key to press. * @param {number} keysym
* @return {Boolean} true if event should NOT be canceled, false otherwise. * The keysym of the key to press.
*
* @return {boolean}
* true if event should NOT be canceled, false otherwise.
*/ */
this.press = function(keysym) { this.press = function(keysym) {
@ -860,7 +850,8 @@ Guacamole.Keyboard = function Keyboard(element) {
/** /**
* Marks a key as released, firing the keyup event if registered. * 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) { 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 * Presses and releases the keys necessary to type the given string of
* text. * text.
* *
* @param {String} str * @param {!string} str
* The string to type. * The string to type.
*/ */
this.type = function type(str) { 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 * Resynchronizes the remote state of the given modifier with its
* remote state of that key with the local state through pressing or * corresponding local modifier state, as dictated by
* {@link KeyEvent#modifiers} within the given key event, by pressing or
* releasing keysyms. * releasing keysyms.
* *
* @private * @private
* @param {Boolean} remoteState * @param {!string} modifier
* Whether the key is currently pressed remotely. * The name of the {@link Guacamole.Keyboard.ModifierState} property
* being updated.
* *
* @param {Boolean} localState * @param {!number[]} keysyms
* Whether the key is currently pressed remotely locally. If the state * The keysyms which represent the modifier being updated.
* of the key is not known, this may be undefined.
* *
* @param {Number[]} keysyms * @param {!KeyEvent} keyEvent
* The keysyms which represent the key being updated.
*
* @param {KeyEvent} keyEvent
* Guacamole's current best interpretation of the key event being * Guacamole's current best interpretation of the key event being
* processed. * processed.
*/ */
var updateModifierState = function updateModifierState(remoteState, var updateModifierState = function updateModifierState(modifier,
localState, keysyms, keyEvent) { keysyms, keyEvent) {
var localState = keyEvent.modifiers[modifier];
var remoteState = guac_keyboard.modifiers[modifier];
var i; var i;
@ -988,56 +980,50 @@ Guacamole.Keyboard = function Keyboard(element) {
}; };
/** /**
* Given a keyboard event, updates the local modifier state and remote * Given a keyboard event, updates the remote key state to match the local
* key state based on the modifier flags within the event. This function * modifier state and remote based on the modifier flags within the event.
* pays no attention to keycodes. * This function pays no attention to keycodes.
* *
* @private * @private
* @param {KeyboardEvent} e * @param {!KeyEvent} keyEvent
* The keyboard event containing the flags to update.
*
* @param {KeyEvent} keyEvent
* Guacamole's current best interpretation of the key event being * Guacamole's current best interpretation of the key event being
* processed. * processed.
*/ */
var syncModifierStates = function syncModifierStates(e, keyEvent) { var syncModifierStates = function syncModifierStates(keyEvent) {
// Get state
var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e);
// Resync state of alt // Resync state of alt
updateModifierState(guac_keyboard.modifiers.alt, state.alt, [ updateModifierState('alt', [
0xFFE9, // Left alt 0xFFE9, // Left alt
0xFFEA, // Right alt 0xFFEA, // Right alt
0xFE03 // AltGr 0xFE03 // AltGr
], keyEvent); ], keyEvent);
// Resync state of shift // Resync state of shift
updateModifierState(guac_keyboard.modifiers.shift, state.shift, [ updateModifierState('shift', [
0xFFE1, // Left shift 0xFFE1, // Left shift
0xFFE2 // Right shift 0xFFE2 // Right shift
], keyEvent); ], keyEvent);
// Resync state of ctrl // Resync state of ctrl
updateModifierState(guac_keyboard.modifiers.ctrl, state.ctrl, [ updateModifierState('ctrl', [
0xFFE3, // Left ctrl 0xFFE3, // Left ctrl
0xFFE4 // Right ctrl 0xFFE4 // Right ctrl
], keyEvent); ], keyEvent);
// Resync state of meta // Resync state of meta
updateModifierState(guac_keyboard.modifiers.meta, state.meta, [ updateModifierState('meta', [
0xFFE7, // Left meta 0xFFE7, // Left meta
0xFFE8 // Right meta 0xFFE8 // Right meta
], keyEvent); ], keyEvent);
// Resync state of hyper // Resync state of hyper
updateModifierState(guac_keyboard.modifiers.hyper, state.hyper, [ updateModifierState('hyper', [
0xFFEB, // Left hyper 0xFFEB, // Left super/hyper
0xFFEC // Right hyper 0xFFEC // Right super/hyper
], keyEvent); ], keyEvent);
// Update state // 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. * inspection of other key events.
* *
* @private * @private
* @returns {Boolean} * @returns {!boolean}
* true if all currently pressed keys were implicitly pressed, false * true if all currently pressed keys were implicitly pressed, false
* otherwise. * otherwise.
*/ */
@ -1068,8 +1054,8 @@ Guacamole.Keyboard = function Keyboard(element) {
* can be). * can be).
* *
* @private * @private
* @return {Boolean} Whether the default action of the latest event should * @return {boolean}
* be prevented. * Whether the default action of the latest event should be prevented.
*/ */
function interpret_events() { function interpret_events() {
@ -1099,7 +1085,8 @@ Guacamole.Keyboard = function Keyboard(element) {
* looks like a key that may require AltGr. * looks like a key that may require AltGr.
* *
* @private * @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) { var release_simulated_altgr = function release_simulated_altgr(keysym) {
@ -1149,6 +1136,29 @@ Guacamole.Keyboard = function Keyboard(element) {
var keysym = null; var keysym = null;
var accepted_events = []; 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 event itself is reliable, no need to wait for other events
if (first.reliable) { if (first.reliable) {
keysym = first.keysym; keysym = first.keysym;
@ -1172,6 +1182,8 @@ Guacamole.Keyboard = function Keyboard(element) {
// Fire a key press if valid events were found // Fire a key press if valid events were found
if (accepted_events.length > 0) { if (accepted_events.length > 0) {
syncModifierStates(first);
if (keysym) { if (keysym) {
// Fire event // Fire event
@ -1213,6 +1225,7 @@ Guacamole.Keyboard = function Keyboard(element) {
return first; return first;
} }
syncModifierStates(first);
return eventLog.shift(); return eventLog.shift();
} // end if keyup } // end if keyup
@ -1233,11 +1246,11 @@ Guacamole.Keyboard = function Keyboard(element) {
* have the same keycode, such as left shift vs. right shift. * have the same keycode, such as left shift vs. right shift.
* *
* @private * @private
* @param {KeyboardEvent} e * @param {!KeyboardEvent} e
* A JavaScript keyboard event, as received through the DOM via a * A JavaScript keyboard event, as received through the DOM via a
* "keydown", "keyup", or "keypress" handler. * "keydown", "keyup", or "keypress" handler.
* *
* @returns {Number} * @returns {!number}
* The location of the key event on the keyboard, as defined at: * The location of the key event on the keyboard, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent * 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, * Guacamole.Keyboard. If the Event has already been marked as handled,
* false is returned. * false is returned.
* *
* @param {Event} e * @param {!Event} e
* The Event to mark. * The Event to mark.
* *
* @returns {Boolean} * @returns {!boolean}
* true if the given Event was successfully marked, false if the given * true if the given Event was successfully marked, false if the given
* Event was already marked. * Event was already marked.
*/ */
@ -1286,7 +1299,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* events signalled through this Guacamole.Keyboard's onkeydown and * events signalled through this Guacamole.Keyboard's onkeydown and
* onkeyup handlers. * onkeyup handlers.
* *
* @param {Element|Document} element * @param {!(Element|Document)} element
* The Element to attach event listeners to for the sake of handling * The Element to attach event listeners to for the sake of handling
* key or input events. * key or input events.
*/ */
@ -1301,17 +1314,11 @@ Guacamole.Keyboard = function Keyboard(element) {
// Ignore events which have already been handled // Ignore events which have already been handled
if (!markEvent(e)) return; if (!markEvent(e)) return;
var keyCode; var keydownEvent = new KeydownEvent(e);
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);
// Ignore (but do not prevent) the "composition" keycode sent by some // 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) // 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; return;
// Log event // Log event
@ -1332,16 +1339,8 @@ Guacamole.Keyboard = function Keyboard(element) {
// Ignore events which have already been handled // Ignore events which have already been handled
if (!markEvent(e)) return; 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 // Log event
eventLog.push(keypressEvent); eventLog.push(new KeypressEvent(e));
// Interpret as many events as possible, prevent default if indicated // Interpret as many events as possible, prevent default if indicated
if (interpret_events()) if (interpret_events())
@ -1360,74 +1359,13 @@ Guacamole.Keyboard = function Keyboard(element) {
e.preventDefault(); 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 // Log event, call for interpretation
eventLog.push(keyupEvent); eventLog.push(new KeyupEvent(e));
interpret_events(); interpret_events();
}, true); }, true);
/** // NEKO: Do not automatically type text entered into the wrapped field
* 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);
}; };
@ -1442,7 +1380,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* instance. * instance.
* *
* @private * @private
* @type {Number} * @type {!number}
*/ */
Guacamole.Keyboard._nextID = 0; Guacamole.Keyboard._nextID = 0;
@ -1454,42 +1392,49 @@ Guacamole.Keyboard.ModifierState = function() {
/** /**
* Whether shift is currently pressed. * Whether shift is currently pressed.
* @type {Boolean} *
* @type {!boolean}
*/ */
this.shift = false; this.shift = false;
/** /**
* Whether ctrl is currently pressed. * Whether ctrl is currently pressed.
* @type {Boolean} *
* @type {!boolean}
*/ */
this.ctrl = false; this.ctrl = false;
/** /**
* Whether alt is currently pressed. * Whether alt is currently pressed.
* @type {Boolean} *
* @type {!boolean}
*/ */
this.alt = false; this.alt = false;
/** /**
* Whether meta (apple key) is currently pressed. * Whether meta (apple key) is currently pressed.
* @type {Boolean} *
* @type {!boolean}
*/ */
this.meta = false; this.meta = false;
/** /**
* Whether hyper (windows key) is currently pressed. * Whether hyper (windows key) is currently pressed.
* @type {Boolean} *
* @type {!boolean}
*/ */
this.hyper = false; this.hyper = false;
}; };
/** /**
* Returns the modifier state applicable to the keyboard event given. * Returns the modifier state applicable to the keyboard event given.
* *
* @param {KeyboardEvent} e The keyboard event to read. * @param {!KeyboardEvent} e
* @returns {Guacamole.Keyboard.ModifierState} The current state of keyboard * The keyboard event to read.
* modifiers. *
* @returns {!Guacamole.Keyboard.ModifierState}
* The current state of keyboard modifiers.
*/ */
Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) { Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) {

View File

@ -8,6 +8,18 @@ export function makeid(length: number) {
return result return result
} }
export function lockKeyboard() {
if (navigator && navigator.keyboard) {
navigator.keyboard.lock()
}
}
export function unlockKeyboard() {
if (navigator && navigator.keyboard) {
navigator.keyboard.unlock()
}
}
export function elementRequestFullscreen(el: HTMLElement) { export function elementRequestFullscreen(el: HTMLElement) {
if (typeof el.requestFullscreen === 'function') { if (typeof el.requestFullscreen === 'function') {
el.requestFullscreen() el.requestFullscreen()

View File

@ -22,6 +22,6 @@ module.exports = {
}, },
}, },
devServer: { devServer: {
disableHostCheck: true, allowedHosts: 'all',
} },
} }

View File

@ -1,26 +1,41 @@
<div align="center"> <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/>
<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/>
<br/> <br/>
</div> </div>
# n.eko # n.eko
This app uses Web RTC to stream a desktop inside of a docker container, original author made this because [rabb.it](https://en.wikipedia.org/wiki/Rabb.it) went under and his internet could not handle streaming and discord kept crashing when his friend attempted to. He just wanted to watch anime with his friends ლ(ಠ益ಠლ) so he started digging throughout the internet and found a few *kinda* clones, but none of them had the virtual browser, then he found [Turtus](https://github.com/Khauri/Turtus) and he was able to figure out the rest. Welcome to Neko, a self-hosted virtual browser that runs in Docker and uses WebRTC technology. Neko is a powerful tool that allows you to **run a fully-functional browser in a virtual environment**, giving you the ability to **access the internet securely and privately from anywhere**. With Neko, you can browse the web, **run applications**, and perform other tasks just as you would on a regular browser, all within a **secure and isolated environment**. Whether you are a developer looking to test web applications, a **privacy-conscious user seeking a secure browsing experience**, or simply someone who wants to take advantage of the **convenience and flexibility of a virtual browser**, Neko is the perfect solution.
In addition to its security and privacy features, Neko offers the **ability for multiple users to access it simultaneously**. This makes it an ideal solution for teams or organizations that need to share access to a browser, as well as for individuals who want to use **multiple devices to access the same virtual environment**. With Neko, you can **easily and securely share access to a browser with others**, without having to worry about maintaining separate configurations or settings. Whether you need to **collaborate on a project**, access shared resources, or simply want to **share access to a browser with friends or family**, Neko makes it easy to do so.
Neko is also a great tool for **hosting watch parties** and interactive presentations. With its virtual browser capabilities, Neko allows you to host watch parties and presentations that are **accessible from anywhere**, without the need for in-person gatherings. This makes it easy to **stay connected with friends and colleagues**, even when you are unable to meet in person. With Neko, you can easily host a watch party or give an **interactive presentation**, whether it's for leisure or work. Simply invite your guests to join the virtual environment, and you can share the screen and **interact with them in real-time**.
## About
This app uses WebRTC to stream a desktop inside of a docker container, original author made this because [rabb.it](https://en.wikipedia.org/wiki/Rabb.it) went under and his internet could not handle streaming and discord kept crashing when his friend attempted to. He just wanted to watch anime with his friends ლ(ಠ益ಠლ) so he started digging throughout the internet and found a few *kinda* clones, but none of them had the virtual browser, then he found [Turtus](https://github.com/Khauri/Turtus) and he was able to figure out the rest.
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. 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.
### Features ### Features
* Text Chat (With basic markdown support, discord flavor) * 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)) * Clipboard synchronization (on [supported browsers](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText))
* Emote overlay * Emote overlay
* Ignore user (chat and emotes) * Ignore user (chat and emotes)
* Persistent settings * 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)
### Why n.eko? ### Why n.eko?

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 237 KiB

Some files were not shown because too many files have changed in this diff Show More