mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
Compare commits
128 Commits
Author | SHA1 | Date | |
---|---|---|---|
a32be0b44a | |||
b2080649ea | |||
851c38b8fd | |||
e2336be568 | |||
e417ec5dbe | |||
ad7e1f2b7b | |||
91e1a8b502 | |||
9bdf9c8851 | |||
c1360d3abc | |||
950095d6d8 | |||
009bc20969 | |||
395db0fd54 | |||
13fa86d543 | |||
8308c13382 | |||
ec175909a3 | |||
d2f51fa10f | |||
70325e0277 | |||
bdff0841ec | |||
3c17dbe282 | |||
887413d536 | |||
f9228e653d | |||
df634be1c5 | |||
2130daa02d | |||
aee7650d47 | |||
98ba32c574 | |||
d08d3eccef | |||
0dd9597519 | |||
334fcef407 | |||
e868ad4061 | |||
b41d0bf956 | |||
b62fa6ab8b | |||
8dba9cff44 | |||
217cc451ea | |||
1f81bd3efc | |||
50e5483661 | |||
d2765c30fd | |||
76fc892823 | |||
ea99ce7753 | |||
646eaced29 | |||
9104953ad9 | |||
c40243635f | |||
8059002475 | |||
9daf83cc52 | |||
0cebe465a2 | |||
d9403d9c14 | |||
8604a30744 | |||
957e893cf6 | |||
55005a6f8d | |||
bc7aae0401 | |||
addb6b6d1b | |||
d2ddae8f45 | |||
724f5fe384 | |||
b0e3e29658 | |||
373dcb4f32 | |||
72c0070a3a | |||
009ceddbd3 | |||
fdf17cfe77 | |||
628c6a1b77 | |||
79a1c41938 | |||
64b79f4579 | |||
8d0468ea62 | |||
89737dd4ce | |||
2649594c2e | |||
f3080713ce | |||
6e62b796fc | |||
4094639ea9 | |||
c45a315d9b | |||
ee13e40d4c | |||
dfe8b8b57d | |||
12623866b3 | |||
161d121e59 | |||
5690a849e2 | |||
cfc6bd417f | |||
32472a70bc | |||
cd9ac70cb9 | |||
a02f47f140 | |||
ccc1df936d | |||
1509521129 | |||
10e2fbd499 | |||
1c228a7daa | |||
e63e9d5b2f | |||
4789b7704f | |||
bd73dfae9d | |||
c8c39b0f83 | |||
61ea7b2940 | |||
4d4e6eaeb7 | |||
2debd4c3f3 | |||
c7885e3a90 | |||
8c54585f2d | |||
e067894e02 | |||
407a85fffb | |||
e0366cf1ef | |||
04a0ce17de | |||
db87229f16 | |||
3df319e071 | |||
ad5abb6054 | |||
ac822a2531 | |||
e25e9c2b24 | |||
1666693c25 | |||
d17a7e8d82 | |||
cdb9b185f2 | |||
76b44b949c | |||
950cb118cc | |||
3703d2b73d | |||
fac8700192 | |||
04cd943eb4 | |||
472a3c3355 | |||
5d41048695 | |||
42ee7477a0 | |||
bbfa0d5834 | |||
4885c2d69e | |||
b65df3e3bf | |||
57e89bb1cc | |||
19af921913 | |||
5ba4019e36 | |||
e3963736ad | |||
b9f31cc19c | |||
7c6029aa99 | |||
cfc7b15211 | |||
70e84c5840 | |||
1505abb703 | |||
758879ef84 | |||
afd0925cfc | |||
7d3a888c79 | |||
cfbda6ec97 | |||
44e891331e | |||
8ab2943126 | |||
fa45619dbb |
@ -1,7 +1,7 @@
|
||||
#
|
||||
# STAGE 1: SERVER
|
||||
#
|
||||
FROM golang:1.18-bullseye as server
|
||||
FROM golang:1.20-bullseye as server
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
@ -32,7 +32,7 @@ RUN go get -v -t -d . && go build -o bin/neko cmd/neko/main.go
|
||||
#
|
||||
# STAGE 2: CLIENT
|
||||
#
|
||||
FROM node:14-bullseye-slim as client
|
||||
FROM node:18-bullseye-slim as client
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
@ -61,11 +61,6 @@ 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
|
||||
@ -73,16 +68,20 @@ RUN set -eux; \
|
||||
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
|
||||
# gst
|
||||
apt-get install -y --no-install-recommends libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio \
|
||||
gstreamer1.0-vaapi ;\
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio; \
|
||||
#
|
||||
# fonts
|
||||
apt-get install -y --no-install-recommends fonts-takao-mincho fonts-wqy-zenhei; \
|
||||
# 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 xfonts-intl-chinese xfonts-wqy \
|
||||
# Korean fonts
|
||||
fonts-wqy-microhei; \
|
||||
#
|
||||
# create a non-root user
|
||||
groupadd --gid $USER_GID $USERNAME; \
|
||||
@ -91,19 +90,18 @@ RUN set -eux; \
|
||||
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; \
|
||||
mkdir -p /etc/neko /var/www /var/log/neko \
|
||||
/tmp/runtime-$USERNAME \
|
||||
/home/$USERNAME/.config/pulse \
|
||||
/home/$USERNAME/.local/share/xorg; \
|
||||
chmod 1777 /var/log/neko; \
|
||||
chown $USERNAME /var/log/neko/; \
|
||||
chown $USERNAME /var/log/neko/ /tmp/runtime-$USERNAME; \
|
||||
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
|
||||
#
|
||||
# clean up
|
||||
@ -116,16 +114,16 @@ COPY .docker/base/dbus /usr/bin/dbus
|
||||
COPY .docker/base/default.pa /etc/pulse/default.pa
|
||||
COPY .docker/base/supervisord.conf /etc/neko/supervisord.conf
|
||||
COPY .docker/base/xorg.conf /etc/neko/xorg.conf
|
||||
COPY .docker/base/add-render-group.sh /usr/bin/add-render-group.sh
|
||||
|
||||
#
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV XDG_RUNTIME_DIR=/tmp/runtime-$USERNAME
|
||||
ENV NEKO_PASSWORD=neko
|
||||
ENV NEKO_PASSWORD_ADMIN=admin
|
||||
ENV NEKO_BIND=:8080
|
||||
ENV RENDER_GID=
|
||||
|
||||
#
|
||||
# copy static files from previous stages
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# STAGE 1: SERVER
|
||||
#
|
||||
FROM arm32v7/golang:1.18-buster as server
|
||||
FROM golang:1.20-bullseye as server
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
@ -13,7 +13,7 @@ RUN set -eux; apt-get update; \
|
||||
# install libclipboard
|
||||
set -eux; \
|
||||
cd /tmp; \
|
||||
git clone https://github.com/jtanx/libclipboard; \
|
||||
git clone --depth=1 https://github.com/jtanx/libclipboard; \
|
||||
cd libclipboard; \
|
||||
cmake .; \
|
||||
make -j4; \
|
||||
@ -32,7 +32,7 @@ RUN go get -v -t -d . && go build -o bin/neko cmd/neko/main.go
|
||||
#
|
||||
# STAGE 2: CLIENT
|
||||
#
|
||||
FROM node:14-buster-slim as client
|
||||
FROM node:18-bullseye-slim as client
|
||||
|
||||
# install dependencies
|
||||
RUN set -eux; apt-get update; \
|
||||
@ -53,7 +53,7 @@ RUN npm run build
|
||||
#
|
||||
# STAGE 3: RUNTIME
|
||||
#
|
||||
FROM arm32v7/debian:buster-slim
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
#
|
||||
# avoid warnings by switching to noninteractive
|
||||
@ -65,19 +65,29 @@ ARG USERNAME=neko
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
RUN set -eux; 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 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
|
||||
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
|
||||
apt-get install -y --no-install-recommends fonts-takao-mincho fonts-wqy-zenhei; \
|
||||
# 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 xfonts-intl-chinese xfonts-wqy \
|
||||
# Korean fonts
|
||||
fonts-wqy-microhei; \
|
||||
#
|
||||
# create a non-root user
|
||||
groupadd --gid $USER_GID $USERNAME; \
|
||||
@ -86,19 +96,18 @@ RUN set -eux; apt-get update; \
|
||||
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; \
|
||||
mkdir -p /etc/neko /var/www /var/log/neko \
|
||||
/tmp/runtime-$USERNAME \
|
||||
/home/$USERNAME/.config/pulse \
|
||||
/home/$USERNAME/.local/share/xorg; \
|
||||
chmod 1777 /var/log/neko; \
|
||||
chown $USERNAME /var/log/neko/; \
|
||||
chown $USERNAME /var/log/neko/ /tmp/runtime-$USERNAME; \
|
||||
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
|
||||
#
|
||||
# clean up
|
||||
@ -116,6 +125,8 @@ COPY .docker/base/xorg.conf /etc/neko/xorg.conf
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV XDG_RUNTIME_DIR=/tmp/runtime-$USERNAME
|
||||
ENV NEKO_PASSWORD=neko
|
||||
ENV NEKO_PASSWORD_ADMIN=admin
|
||||
ENV NEKO_BIND=:8080
|
||||
@ -131,4 +142,3 @@ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
|
||||
#
|
||||
# run neko
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]
|
||||
|
||||
|
150
.docker/base/Dockerfile.intel
Normal file
150
.docker/base/Dockerfile.intel
Normal file
@ -0,0 +1,150 @@
|
||||
#
|
||||
# STAGE 1: SERVER
|
||||
#
|
||||
FROM golang:1.20-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 xfonts-intl-chinese xfonts-wqy \
|
||||
# Korean fonts
|
||||
fonts-wqy-microhei; \
|
||||
#
|
||||
# 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; \
|
||||
#
|
||||
# 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 \
|
||||
/tmp/runtime-$USERNAME \
|
||||
/home/$USERNAME/.config/pulse \
|
||||
/home/$USERNAME/.local/share/xorg; \
|
||||
chmod 1777 /var/log/neko; \
|
||||
chown $USERNAME /var/log/neko/ /tmp/runtime-$USERNAME; \
|
||||
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 PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV XDG_RUNTIME_DIR=/tmp/runtime-$USERNAME
|
||||
ENV NEKO_PASSWORD=neko
|
||||
ENV NEKO_PASSWORD_ADMIN=admin
|
||||
ENV NEKO_BIND=:8080
|
||||
ENV NEKO_HWENC=VAAPI
|
||||
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"]
|
294
.docker/base/Dockerfile.nvidia
Normal file
294
.docker/base/Dockerfile.nvidia
Normal file
@ -0,0 +1,294 @@
|
||||
ARG UBUNTU_RELEASE=20.04
|
||||
ARG CUDA_VERSION=11.4.3
|
||||
ARG VIRTUALGL_VERSION=3.1
|
||||
ARG GSTREAMER_VERSION=1.20
|
||||
|
||||
#
|
||||
# STAGE 0: Build gstreamer with nvidia plugins.
|
||||
#
|
||||
FROM ubuntu:${UBUNTU_RELEASE} AS gstreamer
|
||||
ARG GSTREAMER_VERSION
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Install essentials
|
||||
curl build-essential ca-certificates git \
|
||||
# Install pip and ninja
|
||||
python3-pip python-gi-dev ninja-build \
|
||||
# Install build deps
|
||||
autopoint autoconf automake autotools-dev libtool gettext bison flex gtk-doc-tools \
|
||||
# Install libraries
|
||||
librtmp-dev \
|
||||
libvo-aacenc-dev \
|
||||
libtool-bin \
|
||||
libgtk2.0-dev \
|
||||
libgl1-mesa-dev \
|
||||
libopus-dev \
|
||||
libpulse-dev \
|
||||
libssl-dev \
|
||||
libx264-dev \
|
||||
libvpx-dev; \
|
||||
# Install meson
|
||||
pip3 install meson; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# build gstreamer
|
||||
RUN set -eux; \
|
||||
git clone --depth 1 --branch $GSTREAMER_VERSION https://gitlab.freedesktop.org/gstreamer/gstreamer.git /gstreamer/src; \
|
||||
cd /gstreamer/src; \
|
||||
mkdir -p /opt/gstreamer; \
|
||||
meson --prefix /opt/gstreamer \
|
||||
-Dgpl=enabled \
|
||||
-Dugly=enabled \
|
||||
-Dgst-plugins-ugly:x264=enabled \
|
||||
build; \
|
||||
ninja -C build; \
|
||||
meson install -C build;
|
||||
|
||||
#
|
||||
# STAGE 1: SERVER
|
||||
#
|
||||
FROM golang:1.20-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 nvidia/cuda:${CUDA_VERSION}-runtime-ubuntu${UBUNTU_RELEASE} as runtime
|
||||
|
||||
ARG UBUNTU_RELEASE
|
||||
ARG VIRTUALGL_VERSION
|
||||
|
||||
# Make all NVIDIA GPUs visible by default
|
||||
ENV NVIDIA_VISIBLE_DEVICES all
|
||||
# All NVIDIA driver capabilities should preferably be used, check `NVIDIA_DRIVER_CAPABILITIES` inside the container if things do not work
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES all
|
||||
|
||||
#
|
||||
# set vgl-display to headless 3d gpu card/// correct values are egl[n] or /dev/dri/card0:if this is passed into container
|
||||
ENV VGL_DISPLAY egl
|
||||
|
||||
#
|
||||
# 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; \
|
||||
dpkg --add-architecture i386; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# opengl base: https://gitlab.com/nvidia/container-images/opengl/-/blob/ubuntu20.04/base/Dockerfile
|
||||
libxau6 libxau6:i386 \
|
||||
libxdmcp6 libxdmcp6:i386 \
|
||||
libxcb1 libxcb1:i386 \
|
||||
libxext6 libxext6:i386 \
|
||||
libx11-6 libx11-6:i386 \
|
||||
# opengl runtime: https://gitlab.com/nvidia/container-images/opengl/-/blob/ubuntu20.04/glvnd/runtime/Dockerfile
|
||||
libglvnd0 libglvnd0:i386 \
|
||||
libgl1 libgl1:i386 \
|
||||
libglx0 libglx0:i386 \
|
||||
libegl1 libegl1:i386 \
|
||||
libgles2 libgles2:i386 \
|
||||
# hardware accleration utilities
|
||||
libglu1 libglu1:i386 \
|
||||
libvulkan-dev libvulkan-dev:i386 \
|
||||
mesa-utils mesa-utils-extra \
|
||||
mesa-va-drivers mesa-vulkan-drivers \
|
||||
vainfo vdpauinfo; \
|
||||
#
|
||||
# install vulkan-utils or vulkan-tools depending on ubuntu release
|
||||
if [ "${UBUNTU_RELEASE}" = "18.04" ]; then \
|
||||
apt-get install -y --no-install-recommends vulkan-utils; \
|
||||
else \
|
||||
apt-get install -y --no-install-recommends vulkan-tools; \
|
||||
fi; \
|
||||
#
|
||||
# create symlink for libnvrtc.so (needed for cudaconvert)
|
||||
find /usr/local/cuda/lib64/ -maxdepth 1 -type l -name "*libnvrtc.so.*" -exec sh -c 'ln -sf {} /usr/local/cuda/lib64/libnvrtc.so' \;; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# add cuda to ld path, for gstreamer cuda plugins
|
||||
ENV LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/usr/lib/i386-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}:/usr/local/cuda/lib:/usr/local/cuda/lib64"
|
||||
|
||||
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 pulseaudio dbus-x11 xserver-xorg-video-dummy; \
|
||||
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6 libx264-155 libvo-aacenc0 librtmp1; \
|
||||
apt-get install -y --no-install-recommends libgtk-3-bin software-properties-common cabextract aptitude vim curl; \
|
||||
#
|
||||
# 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 xfonts-intl-chinese xfonts-wqy \
|
||||
# Korean fonts
|
||||
fonts-wqy-microhei; \
|
||||
#
|
||||
# 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; \
|
||||
#
|
||||
# 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 \
|
||||
/tmp/runtime-$USERNAME \
|
||||
/home/$USERNAME/.config/pulse \
|
||||
/home/$USERNAME/.local/share/xorg; \
|
||||
chmod 1777 /var/log/neko; \
|
||||
chown $USERNAME /var/log/neko/ /tmp/runtime-$USERNAME; \
|
||||
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# configure EGL and Vulkan manually
|
||||
RUN VULKAN_API_VERSION=$(dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | grep -oP '[0-9]+(\.[0-9]+)(\.[0-9]+)') && \
|
||||
# Configure EGL manually
|
||||
mkdir -p /usr/share/glvnd/egl_vendor.d/ && \
|
||||
echo "{\n\
|
||||
\"file_format_version\" : \"1.0.0\",\n\
|
||||
\"ICD\": {\n\
|
||||
\"library_path\": \"libEGL_nvidia.so.0\"\n\
|
||||
}\n\
|
||||
}" > /usr/share/glvnd/egl_vendor.d/10_nvidia.json && \
|
||||
# Configure Vulkan manually
|
||||
mkdir -p /etc/vulkan/icd.d/ && \
|
||||
echo "{\n\
|
||||
\"file_format_version\" : \"1.0.0\",\n\
|
||||
\"ICD\": {\n\
|
||||
\"library_path\": \"libGLX_nvidia.so.0\",\n\
|
||||
\"api_version\" : \"${VULKAN_API_VERSION}\"\n\
|
||||
}\n\
|
||||
}" > /etc/vulkan/icd.d/nvidia_icd.json
|
||||
|
||||
#
|
||||
# install VirtualGL and make libraries available for preload
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \
|
||||
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
|
||||
apt-get install -y --no-install-recommends ./virtualgl_${VIRTUALGL_VERSION}_amd64.deb ./virtualgl32_${VIRTUALGL_VERSION}_amd64.deb; \
|
||||
rm -f "virtualgl_${VIRTUALGL_VERSION}_amd64.deb" "virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
|
||||
chmod u+s /usr/lib/libvglfaker.so; \
|
||||
chmod u+s /usr/lib/libdlfaker.so; \
|
||||
chmod u+s /usr/lib32/libvglfaker.so; \
|
||||
chmod u+s /usr/lib32/libdlfaker.so; \
|
||||
chmod u+s /usr/lib/i386-linux-gnu/libvglfaker.so; \
|
||||
chmod u+s /usr/lib/i386-linux-gnu/libdlfaker.so; \
|
||||
#
|
||||
# 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/supervisord.conf /etc/neko/supervisord.conf
|
||||
COPY .docker/base/xorg.conf /etc/neko/xorg.conf
|
||||
COPY .docker/base/nvidia/entrypoint.sh /bin/entrypoint.sh
|
||||
|
||||
#
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV XDG_RUNTIME_DIR=/tmp/runtime-$USERNAME
|
||||
ENV NEKO_PASSWORD=neko
|
||||
ENV NEKO_PASSWORD_ADMIN=admin
|
||||
ENV NEKO_BIND=:8080
|
||||
|
||||
#
|
||||
# set gstreamer envs
|
||||
ENV PATH="/opt/gstreamer/bin:${PATH}"
|
||||
ENV LD_LIBRARY_PATH="/opt/gstreamer/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
||||
ENV PKG_CONFIG_PATH="/opt/gstreamer/lib/x86_64-linux-gnu/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}"
|
||||
|
||||
#
|
||||
# copy gstreamer from previous stage
|
||||
COPY --from=gstreamer /opt/gstreamer /opt/gstreamer
|
||||
|
||||
#
|
||||
# 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"]
|
@ -1,5 +1,8 @@
|
||||
#!/usr/bin/pulseaudio -nF
|
||||
|
||||
### Create virtual output device sink
|
||||
load-module module-null-sink sink_name=audio_output sink_properties=device.description="Virtual\ Audio\ Output"
|
||||
|
||||
# Allow pulse audio to be accessed via TCP (from localhost only), to allow other users to access the virtual devices
|
||||
load-module module-native-protocol-unix socket=/tmp/pulseaudio.socket auth-anonymous=1
|
||||
|
||||
|
69
.docker/base/intel/supervisord.conf
Normal file
69
.docker/base/intel/supervisord.conf
Normal 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
|
12
.docker/base/nvidia/entrypoint.sh
Executable file
12
.docker/base/nvidia/entrypoint.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
# Add VirtualGL directories to path
|
||||
export PATH="${PATH}:/opt/VirtualGL/bin"
|
||||
|
||||
# Use VirtualGL to run wine with OpenGL if the GPU is available, otherwise use barebone wine
|
||||
if [ -n "$(nvidia-smi --query-gpu=uuid --format=csv | sed -n 2p)" ]; then
|
||||
exec vglrun "$@"
|
||||
else
|
||||
echo "No GPU detected"
|
||||
exec "$@"
|
||||
fi
|
@ -9,19 +9,6 @@ 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
|
||||
@ -67,3 +54,14 @@ stdout_logfile=/var/log/neko/neko.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
||||
|
||||
[unix_http_server]
|
||||
file=/var/run/supervisor.sock
|
||||
chmod=0700
|
||||
chown=root:root
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///var/run/supervisor.sock
|
||||
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
23
.docker/brave/Dockerfile.nvidia
Normal file
23
.docker/brave/Dockerfile.nvidia
Normal file
@ -0,0 +1,23 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:nvidia-base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
RUN set -eux; apt-get update; \
|
||||
apt-get install -y --no-install-recommends apt-transport-https curl openbox; \
|
||||
#
|
||||
# install brave browser
|
||||
curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg; \
|
||||
echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg arch=amd64] https://brave-browser-apt-release.s3.brave.com/ stable main" \
|
||||
| tee /etc/apt/sources.list.d/brave-browser-release.list; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends brave-browser; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.nvidia.conf /etc/neko/supervisord/brave.conf
|
||||
COPY --chown=neko preferences.json /home/neko/.config/brave/Default/Preferences
|
||||
COPY policies.json /etc/brave/policies/managed/policies.json
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Brave-browser*" name="brave-browser*">
|
||||
<application class="Brave-browser*" name="brave-browser*" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -18,6 +18,9 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
|
@ -1,6 +1,17 @@
|
||||
[program:brave]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/brave-browser --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/brave --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
|
||||
command=/usr/bin/brave-browser
|
||||
--window-position=0,0
|
||||
--display=%(ENV_DISPLAY)s
|
||||
--user-data-dir=/home/neko/.config/brave
|
||||
--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
|
||||
|
36
.docker/brave/supervisord.nvidia.conf
Normal file
36
.docker/brave/supervisord.nvidia.conf
Normal file
@ -0,0 +1,36 @@
|
||||
[program:brave]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/bin/entrypoint.sh /usr/bin/brave-browser
|
||||
--window-position=0,0
|
||||
--display=%(ENV_DISPLAY)s
|
||||
--user-data-dir=/home/neko/.config/brave
|
||||
--no-first-run
|
||||
--start-maximized
|
||||
--bwsi
|
||||
--force-dark-mode
|
||||
--disable-file-system
|
||||
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
|
||||
--ignore-gpu-blocklist
|
||||
--disable-seccomp-filter-sandbox
|
||||
--use-gl=egl
|
||||
--disable-software-rasterizer
|
||||
--disable-dev-shm-usage
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=800
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/brave.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
|
@ -77,6 +77,36 @@ build_arm() {
|
||||
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
|
||||
}
|
||||
|
||||
build_nvidia() {
|
||||
if [ "$1" = "base" ]
|
||||
then
|
||||
# build nvidia base
|
||||
docker build -t "${BUILD_IMAGE}:nvidia-base" -f base/Dockerfile.nvidia "${BASE}"
|
||||
elif [ -f "$1/Dockerfile.nvidia" ]
|
||||
then
|
||||
# build dedicated nvidia image
|
||||
docker build -t "${BUILD_IMAGE}:nvidia-$1" --build-arg="BASE_IMAGE=${BUILD_IMAGE}:nvidia-base" -f "$1/Dockerfile.nvidia" "$1/"
|
||||
else
|
||||
# try to build nvidia image with common Dockerfile
|
||||
docker build -t "${BUILD_IMAGE}:nvidia-$1" --build-arg="BASE_IMAGE=${BUILD_IMAGE}:nvidia-base" -f "$1/Dockerfile" "$1/"
|
||||
fi
|
||||
}
|
||||
|
||||
case $1 in
|
||||
client) build_client;;
|
||||
server) build_server;;
|
||||
@ -84,6 +114,12 @@ case $1 in
|
||||
# build arm- images
|
||||
arm-*) build_arm "${1#arm-}";;
|
||||
|
||||
# build intel- images
|
||||
intel-*) build_intel "${1#intel-}";;
|
||||
|
||||
# build nvidia- images
|
||||
nvidia-*) build_nvidia "${1#nvidia-}";;
|
||||
|
||||
# build images
|
||||
*) build "$1";;
|
||||
esac
|
||||
|
@ -4,7 +4,7 @@ FROM $BASE_IMAGE
|
||||
#
|
||||
# install neko chromium
|
||||
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; \
|
||||
#
|
||||
# clean up
|
||||
|
37
.docker/chromium/Dockerfile.nvidia
Normal file
37
.docker/chromium/Dockerfile.nvidia
Normal file
@ -0,0 +1,37 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
#
|
||||
# install neko chromium
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends software-properties-common; \
|
||||
# chromium-browser from default repo needs snap to be installed
|
||||
# and nvidia base is ubuntu not debian
|
||||
add-apt-repository ppa:system76/pop; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends unzip chromium openbox; \
|
||||
#
|
||||
# install widevine module
|
||||
CHROMIUM_DIR="/usr/lib/chromium"; \
|
||||
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
|
||||
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
|
||||
mkdir -p "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64"; \
|
||||
unzip -p /tmp/widevine.zip LICENSE.txt > "${CHROMIUM_DIR}/WidevineCdm/LICENSE"; \
|
||||
unzip -p /tmp/widevine.zip manifest.json > "${CHROMIUM_DIR}/WidevineCdm/manifest.json"; \
|
||||
unzip -p /tmp/widevine.zip libwidevinecdm.so > "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so"; \
|
||||
find "${CHROMIUM_DIR}/WidevineCdm" -type d -exec chmod 0755 '{}' \;; \
|
||||
find "${CHROMIUM_DIR}/WidevineCdm" -type f -exec chmod 0644 '{}' \;; \
|
||||
rm /tmp/widevine.zip; \
|
||||
#
|
||||
# clean up
|
||||
apt-get --purge autoremove -y unzip; \
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.nvidia.conf /etc/neko/supervisord/chromium.conf
|
||||
COPY --chown=neko preferences.json /home/neko/.config/chromium/Default/Preferences
|
||||
COPY policies.json /etc/chromium/policies/managed/policies.json
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Chromium*" name="chromium*">
|
||||
<application class="Chromium*" name="chromium*" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -19,6 +19,9 @@
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"BrowserLabsEnabled": false,
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
|
@ -1,6 +1,17 @@
|
||||
[program:chromium]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/chromium --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/chromium --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
|
||||
command=/usr/bin/chromium
|
||||
--window-position=0,0
|
||||
--display=%(ENV_DISPLAY)s
|
||||
--user-data-dir=/home/neko/.config/chromium
|
||||
--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
|
||||
|
36
.docker/chromium/supervisord.nvidia.conf
Normal file
36
.docker/chromium/supervisord.nvidia.conf
Normal file
@ -0,0 +1,36 @@
|
||||
[program:chromium]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/bin/entrypoint.sh /usr/bin/chromium
|
||||
--window-position=0,0
|
||||
--display=%(ENV_DISPLAY)s
|
||||
--user-data-dir=/home/neko/.config/chromium
|
||||
--no-first-run
|
||||
--start-maximized
|
||||
--bwsi
|
||||
--force-dark-mode
|
||||
--disable-file-system
|
||||
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
|
||||
--ignore-gpu-blocklist
|
||||
--disable-seccomp-filter-sandbox
|
||||
--use-gl=egl
|
||||
--disable-software-rasterizer
|
||||
--disable-dev-shm-usage
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=800
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/chromium.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
|
35
.docker/firefox/Dockerfile.nvidia
Normal file
35
.docker/firefox/Dockerfile.nvidia
Normal file
@ -0,0 +1,35 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
ARG SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US"
|
||||
|
||||
#
|
||||
# install firefox
|
||||
RUN set -eux; apt-get update; \
|
||||
apt-get install -y --no-install-recommends openbox \
|
||||
xz-utils bzip2 libgtk-3-0 libdbus-glib-1-2; \
|
||||
#
|
||||
# fetch latest release
|
||||
wget -O /tmp/firefox-setup.tar.bz2 "${SRC_URL}"; \
|
||||
mkdir /usr/lib/firefox; \
|
||||
tar -xjf /tmp/firefox-setup.tar.bz2 -C /usr/lib; \
|
||||
rm -f /tmp/firefox-setup.tar.bz2; \
|
||||
ln -s /usr/lib/firefox/firefox /usr/bin/firefox; \
|
||||
#
|
||||
# create a profile directory
|
||||
mkdir -p /home/neko/.mozilla/firefox/profile.default/extensions; \
|
||||
chown -R neko:neko /home/neko/.mozilla/firefox/profile.default; \
|
||||
#
|
||||
# clean up
|
||||
apt-get --purge autoremove -y xz-utils bzip2; \
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.nvidia.conf /etc/neko/supervisord/firefox.conf
|
||||
COPY neko.js /usr/lib/firefox/mozilla.cfg
|
||||
COPY autoconfig.js /usr/lib/firefox/defaults/pref/autoconfig.js
|
||||
COPY policies.json /usr/lib/firefox/distribution/policies.json
|
||||
COPY --chown=neko profiles.ini /home/neko/.mozilla/firefox/profiles.ini
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="firefox" name="Navigator">
|
||||
<application class="firefox" name="Navigator" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
28
.docker/firefox/supervisord.nvidia.conf
Normal file
28
.docker/firefox/supervisord.nvidia.conf
Normal file
@ -0,0 +1,28 @@
|
||||
[program:firefox]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/bin/entrypoint.sh /usr/bin/firefox
|
||||
--no-remote
|
||||
-P default
|
||||
--display=%(ENV_DISPLAY)s
|
||||
-setDefaultBrowser
|
||||
-width 1280
|
||||
-height 720
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=800
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/firefox.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
|
23
.docker/google-chrome/Dockerfile.nvidia
Normal file
23
.docker/google-chrome/Dockerfile.nvidia
Normal file
@ -0,0 +1,23 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:nvidia-base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
# latest working version with EGL: 111.0.5563.146, revert when resolved
|
||||
# 112.0.5615.49 fails: https://github.com/VirtualGL/virtualgl/issues/229
|
||||
ARG SRC_URL="https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_111.0.5563.146-1_amd64.deb"
|
||||
|
||||
#
|
||||
# install google chrome
|
||||
RUN set -eux; apt-get update; \
|
||||
wget -O /tmp/google-chrome.deb "${SRC_URL}"; \
|
||||
apt-get install -y --no-install-recommends openbox /tmp/google-chrome.deb; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.nvidia.conf /etc/neko/supervisord/google-chrome.conf
|
||||
COPY --chown=neko preferences.json /home/neko/.config/google-chrome/Default/Preferences
|
||||
COPY policies.json /etc/opt/chrome/policies/managed/policies.json
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Google-chrome*" name="google-chrome*">
|
||||
<application class="Google-chrome*" name="google-chrome*" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -18,6 +18,9 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
|
@ -1,6 +1,17 @@
|
||||
[program:google-chrome]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/google-chrome --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/google-chrome --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
|
||||
command=/usr/bin/google-chrome
|
||||
--window-position=0,0
|
||||
--display=%(ENV_DISPLAY)s
|
||||
--user-data-dir=/home/neko/.config/google-chrome
|
||||
--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
|
||||
|
36
.docker/google-chrome/supervisord.nvidia.conf
Normal file
36
.docker/google-chrome/supervisord.nvidia.conf
Normal file
@ -0,0 +1,36 @@
|
||||
[program:google-chrome]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/bin/entrypoint.sh /usr/bin/google-chrome
|
||||
--window-position=0,0
|
||||
--display=%(ENV_DISPLAY)s
|
||||
--user-data-dir=/home/neko/.config/chromium
|
||||
--no-first-run
|
||||
--start-maximized
|
||||
--bwsi
|
||||
--force-dark-mode
|
||||
--disable-file-system
|
||||
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
|
||||
--ignore-gpu-blocklist
|
||||
--disable-seccomp-filter-sandbox
|
||||
--use-gl=egl
|
||||
--disable-software-rasterizer
|
||||
--disable-dev-shm-usage
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=800
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/google-chrome.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
|
26
.docker/kde/Dockerfile
Normal file
26
.docker/kde/Dockerfile
Normal file
@ -0,0 +1,26 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
#
|
||||
# install kde
|
||||
RUN set -eux; apt-get update; \
|
||||
apt-get install -y --no-install-recommends kde-full kwin-x11 sudo; \
|
||||
#
|
||||
# add user to sudoers
|
||||
usermod -aG sudo neko; \
|
||||
echo "neko:neko" | chpasswd; \
|
||||
echo "%sudo ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \
|
||||
# clean up
|
||||
apt remove xserver-xorg-legacy -y; \
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# disable autolock
|
||||
RUN kwriteconfig5 --file /home/neko/.config/kscreenlockerrc --group Daemon --key Autolock false; \
|
||||
chown neko:neko /home/neko/.config/kscreenlockerrc
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.conf /etc/neko/supervisord/kde.conf
|
||||
|
23
.docker/kde/supervisord.conf
Normal file
23
.docker/kde/supervisord.conf
Normal file
@ -0,0 +1,23 @@
|
||||
[program:kde]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/startplasma-x11
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=500
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/kde.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
||||
|
||||
[program:kwin]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/kwin_x11
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=500
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/kwin.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
24
.docker/microsoft-edge/Dockerfile.nvidia
Normal file
24
.docker/microsoft-edge/Dockerfile.nvidia
Normal file
@ -0,0 +1,24 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
ARG API_URL="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/"
|
||||
|
||||
#
|
||||
# install microsoft edge
|
||||
RUN set -eux; apt-get update; \
|
||||
#
|
||||
# fetch latest release
|
||||
SRC_URL="${API_URL}$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"]*\).*/\1/p' | tail -1)"; \
|
||||
wget -O /tmp/microsoft-edge.deb "${SRC_URL}"; \
|
||||
apt-get install -y --no-install-recommends openbox /tmp/microsoft-edge.deb; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.nvidia.conf /etc/neko/supervisord/microsoft-edge.conf
|
||||
COPY --chown=neko preferences.json /home/neko/.config/microsoft-edge/Default/Preferences
|
||||
COPY policies.json /etc/opt/edge/policies/managed/policies.json
|
||||
COPY openbox.xml /etc/neko/openbox.xml
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- 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>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -18,6 +18,9 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"edge://policy"
|
||||
|
@ -1,6 +1,17 @@
|
||||
[program:microsoft-edge]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/microsoft-edge --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/microsoft-edge --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
|
||||
command=/usr/bin/microsoft-edge
|
||||
--window-position=0,0
|
||||
--display=%(ENV_DISPLAY)s
|
||||
--user-data-dir=/home/neko/.config/microsoft-edge
|
||||
--no-first-run
|
||||
--start-maximized
|
||||
--bwsi
|
||||
--force-dark-mode
|
||||
--disable-file-system
|
||||
--disable-gpu
|
||||
--disable-software-rasterizer
|
||||
--disable-dev-shm-usage
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=800
|
||||
|
36
.docker/microsoft-edge/supervisord.nvidia.conf
Normal file
36
.docker/microsoft-edge/supervisord.nvidia.conf
Normal file
@ -0,0 +1,36 @@
|
||||
[program:microsoft-edge]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/bin/entrypoint.sh /usr/bin/microsoft-edge
|
||||
--window-position=0,0
|
||||
--display=%(ENV_DISPLAY)s
|
||||
--user-data-dir=/home/neko/.config/microsoft-edge
|
||||
--no-first-run
|
||||
--start-maximized
|
||||
--bwsi
|
||||
--force-dark-mode
|
||||
--disable-file-system
|
||||
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
|
||||
--ignore-gpu-blocklist
|
||||
--disable-seccomp-filter-sandbox
|
||||
--use-gl=egl
|
||||
--disable-software-rasterizer
|
||||
--disable-dev-shm-usage
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=800
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/microsoft-edge.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
||||
|
||||
[program:openbox]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
|
||||
autorestart=true
|
||||
priority=300
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/openbox.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
@ -1,24 +1,28 @@
|
||||
ARG BASE_IMAGE=m1k1o/neko:base
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
ARG SRC_URL="https://download.opera.com/download/get/?id=58545&location=415¬hanks=yes&sub=marine&utm_tryagain=yes"
|
||||
|
||||
ARG LIBFFMPEG_URL="https://github.com/nwjs-ffmpeg-prebuilt/nwjs-ffmpeg-prebuilt/releases/download/0.67.1/0.67.1-linux-x64.zip"
|
||||
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 apt-get update
|
||||
RUN wget -O /tmp/opera.deb $SRC_URL
|
||||
RUN apt-get install -y --no-install-recommends openbox unzip /tmp/opera.deb
|
||||
|
||||
## install libffmpeg
|
||||
RUN wget -O /tmp/libffmpeg.zip $LIBFFMPEG_URL
|
||||
RUN unzip -o /tmp/libffmpeg.zip libffmpeg.so -d /usr/lib/x86_64-linux-gnu/opera/lib_extra
|
||||
RUN echo '[]' > /usr/lib/x86_64-linux-gnu/opera/resources/ffmpeg_preload_config.json
|
||||
#
|
||||
# clean up
|
||||
RUN apt-get clean -y
|
||||
RUN rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
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
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Opera" name="Opera">
|
||||
<application class="Opera" name="Opera" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -17,5 +17,9 @@ fi
|
||||
|
||||
docker run --rm -it \
|
||||
-v "${PWD}/../server:/src" \
|
||||
--entrypoint="go" \
|
||||
neko_dev_server build -o "bin/neko" "cmd/neko/main.go"
|
||||
-e GIT_COMMIT=`git rev-parse --short HEAD` \
|
||||
-e GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD` \
|
||||
-e GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"` \
|
||||
--entrypoint="bash" \
|
||||
--workdir="/src" \
|
||||
neko_dev_server ./build
|
||||
|
@ -16,7 +16,7 @@ if [ ! -d "${PWD}/../client/node_modules" ] || [ "$1" == "-i" ]; then
|
||||
-v "${PWD}/../client:/app" \
|
||||
--workdir="/app" \
|
||||
--entrypoint="npm" \
|
||||
node:14-buster-slim install
|
||||
node:18-bullseye-slim install
|
||||
fi
|
||||
|
||||
docker run --rm -it \
|
||||
@ -25,5 +25,5 @@ docker run --rm -it \
|
||||
-e "VUE_APP_SERVER_PORT=${SERVER_PORT}" \
|
||||
--workdir="/app" \
|
||||
--entrypoint="npm" \
|
||||
node:14-buster-slim run serve
|
||||
node:18-bullseye-slim run serve
|
||||
|
@ -17,8 +17,22 @@ if [ ! -f "${BINARY_PATH}" ] || [ "$1" == "-r" ]; then
|
||||
./rebuild-server
|
||||
fi
|
||||
|
||||
# if image starts with nvidia- add --gpus all
|
||||
if [[ "${SERVER_TAG}" == "nvidia-"* ]]; then
|
||||
GPU_FLAG="--gpus all"
|
||||
echo "Nvidia GPU acceleration enabled"
|
||||
fi
|
||||
|
||||
# if image starts with intel- add --device /dev/dri
|
||||
if [[ "${SERVER_TAG}" == "intel-"* ]]; then
|
||||
GPU_FLAG="--device /dev/dri"
|
||||
echo "Intel GPU acceleration enabled"
|
||||
fi
|
||||
|
||||
# use --gpus all to enable GPU acceleration
|
||||
docker run --rm -it \
|
||||
--name "neko_dev" \
|
||||
$GPU_FLAG \
|
||||
-p "${SERVER_PORT}:8080" \
|
||||
-p "${SERVER_EPR}:${SERVER_EPR}/udp" \
|
||||
-e "NEKO_SCREEN=1920x1080@60" \
|
||||
|
@ -11,6 +11,7 @@ RUN set -eux; apt-get update; \
|
||||
echo "Downloading $DOWNLOAD_URI"; \
|
||||
curl -sSL -o /tmp/tor.tar.xz "https://www.torproject.org/$DOWNLOAD_URI"; \
|
||||
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/; \
|
||||
rm -f /tmp/tor.tar.xz; \
|
||||
#
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<applications>
|
||||
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
|
||||
<application class="Chromium*" name="chromium-devel">
|
||||
<application class="Chromium*" name="chromium-devel" role="browser">
|
||||
<decor>no</decor>
|
||||
<maximized>true</maximized>
|
||||
<focus>yes</focus>
|
||||
|
@ -18,6 +18,9 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
|
@ -18,6 +18,9 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLAllowlist": [
|
||||
"file:///home/neko/Downloads"
|
||||
],
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
|
@ -1,4 +1,4 @@
|
||||
name: "CI for builds"
|
||||
name: "build and push amd64 images to Docker Hub"
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -50,7 +50,7 @@ jobs:
|
||||
# Will build all images even if some fail.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tags: [ firefox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, vivaldi, opera, tor-browser, remmina, vlc, xfce ]
|
||||
tags: [ firefox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, vivaldi, opera, tor-browser, remmina, vlc, xfce, kde ]
|
||||
env:
|
||||
DOCKER_TAG: ${{ matrix.tags }}
|
||||
steps:
|
@ -1,4 +1,4 @@
|
||||
name: "CI for version tags"
|
||||
name: "amd64 images"
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -8,6 +8,9 @@ on:
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: m1k1o/neko
|
||||
TAG_PREFIX: ""
|
||||
BASE_DOCKERFILE: Dockerfile
|
||||
PLATFORMS: linux/amd64
|
||||
|
||||
jobs:
|
||||
build-base:
|
||||
@ -31,7 +34,7 @@ jobs:
|
||||
uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/base
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
@ -49,8 +52,8 @@ jobs:
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: .docker/base/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: .docker/base/${{ env.BASE_DOCKERFILE }}
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@ -67,32 +70,20 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- tag: firefox
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: chromium
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: google-chrome
|
||||
platforms: linux/amd64
|
||||
- tag: ungoogled-chromium
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: microsoft-edge
|
||||
platforms: linux/amd64
|
||||
- tag: brave
|
||||
platforms: linux/amd64
|
||||
- tag: vivaldi
|
||||
platforms: linux/amd64
|
||||
- tag: opera
|
||||
platforms: linux/amd64
|
||||
- tag: tor-browser
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: remmina
|
||||
platforms: linux/amd64
|
||||
- tag: vlc
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: xfce
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- tag: kde
|
||||
env:
|
||||
TAG_NAME: ${{ matrix.tag }}
|
||||
PLATFORMS: ${{ matrix.platforms }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@ -108,11 +99,12 @@ jobs:
|
||||
uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_NAME }}
|
||||
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
|
||||
@ -130,4 +122,4 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
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
126
.github/workflows/ghcr-arm.yml
vendored
Normal 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 }}
|
126
.github/workflows/ghcr-intel.yml
vendored
Normal file
126
.github/workflows/ghcr-intel.yml
vendored
Normal file
@ -0,0 +1,126 @@
|
||||
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
|
||||
- tag: kde
|
||||
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 }}
|
124
.github/workflows/ghcr-nvidia.yml
vendored
Normal file
124
.github/workflows/ghcr-nvidia.yml
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
name: "nvidia gpu supported images"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: m1k1o/neko
|
||||
TAG_PREFIX: nvidia-
|
||||
BASE_DOCKERFILE: Dockerfile.nvidia
|
||||
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
|
||||
dockerfile: Dockerfile.nvidia
|
||||
- tag: brave
|
||||
dockerfile: Dockerfile.nvidia
|
||||
- tag: chromium
|
||||
dockerfile: Dockerfile.nvidia
|
||||
- tag: google-chrome
|
||||
dockerfile: Dockerfile.nvidia
|
||||
- tag: microsoft-edge
|
||||
dockerfile: Dockerfile.nvidia
|
||||
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 }}
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -23,10 +23,6 @@ pids
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Lock files
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# TypeScript incremental compilation cache
|
||||
*.tsbuildinfo
|
||||
|
||||
|
29
README.md
29
README.md
@ -22,33 +22,41 @@
|
||||
<img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord">
|
||||
</a>
|
||||
<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>
|
||||
</p>
|
||||
<img src="https://i.imgur.com/ZSzbQr7.gif" width="650" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/intro.gif" width="650" height="auto"/>
|
||||
</div>
|
||||
|
||||
# n.eko
|
||||
|
||||
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 multiple 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).
|
||||
- In fact, it is not limited to a single program either; you can install a full desktop environment (e.g. XFCE, KDE).
|
||||
- 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/).
|
||||
- **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](https://hyperbeam.com/).
|
||||
- **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).
|
||||
|
||||
@ -91,6 +99,7 @@ Compared to clientless remote desktop gateway (e.g. [Apache Guacamole](https://g
|
||||
<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"/>
|
||||
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/kde.svg" title="m1k1o/neko:kde" width="60" height="auto"/>
|
||||
|
||||
... others in <a href="https://github.com/m1k1o/neko-apps">m1k1o/neko-apps</a>
|
||||
</div>
|
||||
@ -105,6 +114,14 @@ Compared to clientless remote desktop gateway (e.g. [Apache Guacamole](https://g
|
||||
* 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?
|
||||
|
||||
|
21271
client/package-lock.json
generated
Normal file
21271
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -20,18 +20,18 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.1.2",
|
||||
"@fortawesome/fontawesome-free": "^6.2.0",
|
||||
"animejs": "^3.2.0",
|
||||
"axios": "^0.21.4",
|
||||
"date-fns": "^2.29.1",
|
||||
"axios": "^1.2.3",
|
||||
"date-fns": "^2.29.3",
|
||||
"emoji-datasource": "^6.0.1",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"simple-markdown": "^0.7.2",
|
||||
"sweetalert2": "^11.4.24",
|
||||
"sweetalert2": "11.4.8",
|
||||
"typed-vuex": "^0.1.21",
|
||||
"v-tooltip": "^2.0.3",
|
||||
"vue": "^2.7.8",
|
||||
"vue": "^2.7.13",
|
||||
"vue-class-component": "^7.2.6",
|
||||
"vue-clickaway": "^2.2.2",
|
||||
"vue-context": "^5.2.0",
|
||||
@ -41,29 +41,29 @@
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/animejs": "^3.1.5",
|
||||
"@types/node": "^14.18.23",
|
||||
"@types/animejs": "^3.1.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/vue": "^2.0.0",
|
||||
"@types/vue-clickaway": "^2.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"@vue/cli-plugin-babel": "^4.5.19",
|
||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
||||
"@vue/cli-plugin-typescript": "^4.5.19",
|
||||
"@vue/cli-plugin-vuex": "^4.5.19",
|
||||
"@vue/cli-service": "^4.5.19",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.8",
|
||||
"@typescript-eslint/parser": "^5.0.8",
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/cli-plugin-eslint": "^5.0.8",
|
||||
"@vue/cli-plugin-typescript": "^5.0.8",
|
||||
"@vue/cli-plugin-vuex": "^5.0.8",
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"@vue/eslint-config-typescript": "^7.0.0",
|
||||
"core-js": "^3.24.1",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"core-js": "^3.26.0",
|
||||
"emojilib": "^3.0.7",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-vue": "^7.20.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.54.0",
|
||||
"sass": "^1.55.0",
|
||||
"sass-loader": "^10.3.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vue-template-compiler": "^2.7.8"
|
||||
"typescript": "^4.8.4",
|
||||
"vue-template-compiler": "^2.7.13"
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,22 @@
|
||||
<template>
|
||||
<div id="neko" :class="[side ? 'expanded' : '']">
|
||||
<div id="neko" :class="[!videoOnly && side ? 'expanded' : '']">
|
||||
<template v-if="!$client.supported">
|
||||
<neko-unsupported />
|
||||
</template>
|
||||
<template v-else>
|
||||
<main class="neko-main">
|
||||
<div v-if="!hideControls" class="header-container">
|
||||
<div v-if="!videoOnly" class="header-container">
|
||||
<neko-header />
|
||||
</div>
|
||||
<div class="video-container">
|
||||
<neko-video ref="video" :hideControls="hideControls" @control-attempt="controlAttempt" />
|
||||
<neko-video
|
||||
ref="video"
|
||||
:hideControls="hideControls"
|
||||
:extraControls="isEmbedMode"
|
||||
@control-attempt="controlAttempt"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!hideControls" class="room-container">
|
||||
<div v-if="!videoOnly" class="room-container">
|
||||
<neko-members />
|
||||
<div class="room-menu">
|
||||
<div class="settings">
|
||||
@ -26,11 +31,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<neko-side v-if="!hideControls && side" />
|
||||
<neko-side v-if="!videoOnly && side" />
|
||||
<neko-connect v-if="!connected" />
|
||||
<neko-about v-if="about" />
|
||||
<notifications
|
||||
v-if="!hideControls"
|
||||
v-if="!videoOnly"
|
||||
group="neko"
|
||||
position="top left"
|
||||
style="top: 50px; pointer-events: none"
|
||||
@ -176,10 +181,32 @@
|
||||
|
||||
shakeKbd = false
|
||||
|
||||
get hideControls() {
|
||||
get volume() {
|
||||
const numberParam = parseFloat(new URL(location.href).searchParams.get('volume') || '1.0')
|
||||
return Math.max(0.0, Math.min(!isNaN(numberParam) ? numberParam * 100 : 100, 100))
|
||||
}
|
||||
|
||||
get isCastMode() {
|
||||
return !!new URL(location.href).searchParams.get('cast')
|
||||
}
|
||||
|
||||
get isEmbedMode() {
|
||||
return !!new URL(location.href).searchParams.get('embed')
|
||||
}
|
||||
|
||||
get hideControls() {
|
||||
return this.isCastMode
|
||||
}
|
||||
|
||||
get videoOnly() {
|
||||
return this.isCastMode || this.isEmbedMode
|
||||
}
|
||||
|
||||
@Watch('volume', { immediate: true })
|
||||
onVolume(volume: number) {
|
||||
this.$accessor.video.setVolume(volume)
|
||||
}
|
||||
|
||||
@Watch('hideControls', { immediate: true })
|
||||
onHideControls(enabled: boolean) {
|
||||
if (enabled) {
|
||||
|
@ -132,8 +132,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||
import md, { HtmlOutputRule } from 'simple-markdown'
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component({ name: 'neko-about' })
|
||||
export default class extends Vue {
|
||||
|
@ -147,8 +147,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||
import { get, set } from '~/utils/localstorage'
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component({ name: 'neko-connect' })
|
||||
export default class extends Vue {
|
||||
|
@ -132,7 +132,7 @@
|
||||
</style>
|
||||
|
||||
<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'
|
||||
|
||||
// @ts-ignore
|
||||
@ -229,11 +229,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
adminRelease(member: Member) {
|
||||
adminRelease() {
|
||||
this.$accessor.remote.adminRelease()
|
||||
}
|
||||
|
||||
adminControl(member: Member) {
|
||||
adminControl() {
|
||||
this.$accessor.remote.adminControl()
|
||||
}
|
||||
|
||||
|
@ -287,9 +287,9 @@
|
||||
</style>
|
||||
|
||||
<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 { get, set } from '../utils/localstorage'
|
||||
import { get } from '../utils/localstorage'
|
||||
|
||||
@Component({
|
||||
name: 'neko-emoji',
|
||||
@ -356,7 +356,7 @@
|
||||
this.waitingForPaint = false
|
||||
let scrollTop = this._scroll.scrollTop
|
||||
let active = 0
|
||||
for (const [i, group] of this.groups.entries()) {
|
||||
for (const [i] of this.groups.entries()) {
|
||||
let component = this._groups[i]
|
||||
if (component && component.offsetTop > scrollTop) {
|
||||
break
|
||||
@ -368,7 +368,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMouseExit(event: MouseEvent, emoji: string) {
|
||||
onMouseExit() {
|
||||
this.hovered = ''
|
||||
}
|
||||
|
||||
@ -382,7 +382,7 @@
|
||||
this.$emit('picked', emoji)
|
||||
}
|
||||
|
||||
onClickAway(event: MouseEvent) {
|
||||
onClickAway() {
|
||||
this.$emit('done')
|
||||
}
|
||||
}
|
||||
|
520
client/src/components/files.vue
Normal file
520
client/src/components/files.vue
Normal file
@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<div class="files">
|
||||
<div class="files-cwd">
|
||||
<p>{{ cwd }}</p>
|
||||
<i class="fas fa-rotate-right refresh" @click="refresh" />
|
||||
</div>
|
||||
<div class="files-list">
|
||||
<div v-for="item in files" :key="item.name" class="files-list-item">
|
||||
<i :class="fileIcon(item)" />
|
||||
<p class="file-name" :title="item.name">{{ item.name }}</p>
|
||||
<p class="file-size">{{ fileSize(item.size) }}</p>
|
||||
<i v-if="item.type !== 'dir'" class="fas fa-download download" @click="download(item)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="transfer-area">
|
||||
<div class="transfers" v-if="transfers.length > 0">
|
||||
<p v-if="downloads.length > 0" class="transfers-list-header">
|
||||
<span>{{ $t('files.downloads') }}</span>
|
||||
<i class="fas fa-xmark remove-transfer" @click="downloads.forEach((t) => removeTransfer(t))"></i>
|
||||
</p>
|
||||
<div v-for="download in downloads" :key="download.id" class="transfers-list-item">
|
||||
<div class="transfer-info">
|
||||
<i
|
||||
class="fas transfer-status"
|
||||
:class="{
|
||||
'fa-clock': download.status === 'pending',
|
||||
'fa-arrows-rotate': download.status === 'inprogress',
|
||||
'fa-check': download.status === 'completed',
|
||||
'fa-warning': download.status === 'failed',
|
||||
}"
|
||||
></i>
|
||||
<p class="file-name" :title="download.name">{{ download.name }}</p>
|
||||
<p class="file-size">{{ Math.min(100, Math.round((download.progress / download.size) * 100)) }}%</p>
|
||||
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(download)"></i>
|
||||
</div>
|
||||
<div v-if="download.status === 'failed'" class="transfer-error">{{ download.error }}</div>
|
||||
<progress
|
||||
v-else
|
||||
class="transfer-progress"
|
||||
:aria-label="download.name + ' progress'"
|
||||
:value="download.progress"
|
||||
:max="download.size"
|
||||
></progress>
|
||||
</div>
|
||||
<p v-if="uploads.length > 0" class="transfers-list-header">
|
||||
<span>{{ $t('files.uploads') }}</span>
|
||||
<i class="fas fa-xmark remove-transfer" @click="uploads.forEach((t) => removeTransfer(t))"></i>
|
||||
</p>
|
||||
<div v-for="upload in uploads" :key="upload.id" class="transfers-list-item">
|
||||
<div class="transfer-info">
|
||||
<i
|
||||
class="fas transfer-status"
|
||||
:title="upload.status"
|
||||
:class="{
|
||||
'fa-clock': upload.status === 'pending',
|
||||
'fa-arrows-rotate': upload.status === 'inprogress',
|
||||
'fa-check': upload.status === 'completed',
|
||||
'fa-warning': upload.status === 'failed',
|
||||
}"
|
||||
></i>
|
||||
<p class="file-name" :title="upload.name">{{ upload.name }}</p>
|
||||
<p class="file-size">{{ Math.min(100, Math.round((upload.progress / upload.size) * 100)) }}%</p>
|
||||
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(upload)"></i>
|
||||
</div>
|
||||
<div v-if="upload.status === 'failed'" class="transfer-error">{{ upload.error }}</div>
|
||||
<progress
|
||||
v-else
|
||||
class="transfer-progress"
|
||||
:aria-label="upload.name + ' progress'"
|
||||
:value="upload.progress"
|
||||
:max="upload.size"
|
||||
></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="upload-area"
|
||||
:class="{ 'upload-area-drag': uploadAreaDrag }"
|
||||
@dragover.prevent="uploadAreaDrag = true"
|
||||
@dragleave.prevent="uploadAreaDrag = false"
|
||||
@drop.prevent="(e) => upload(e.dataTransfer)"
|
||||
@click="openFileBrowser"
|
||||
>
|
||||
<i class="fas fa-file-arrow-up" />
|
||||
<p>{{ $t('files.upload_here') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.files {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
|
||||
.files-cwd {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 10px 10px 0px 10px;
|
||||
padding: 0.5em;
|
||||
font-weight: 600;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.files-list {
|
||||
margin: 10px 10px 10px 10px;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 5px;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $background-tertiary transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $background-tertiary;
|
||||
border: 2px solid $background-primary;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: $background-floating;
|
||||
}
|
||||
}
|
||||
|
||||
.files-list-item {
|
||||
padding: 0.5em;
|
||||
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.transfers-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
|
||||
}
|
||||
|
||||
.file-icon,
|
||||
.transfer-status {
|
||||
width: 14px;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.transfer-error {
|
||||
border: 1px solid $style-error;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.files-list-item:last-child {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.refresh {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
margin-left: auto;
|
||||
margin-right: 0.5em;
|
||||
color: rgba($color: #fff, $alpha: 0.4);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.refresh:hover,
|
||||
.download:hover,
|
||||
.remove-transfer:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.transfer-area {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.transfers {
|
||||
margin: 10px 10px 10px 10px;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 5px;
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $background-tertiary transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $background-tertiary;
|
||||
border: 2px solid $background-primary;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: $background-floating;
|
||||
}
|
||||
}
|
||||
|
||||
.transfers > p {
|
||||
padding: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.transfer-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.transfer-progress {
|
||||
margin: 0px 10px 10px 10px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
margin: 10px 10px 10px 10px;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-area-drag,
|
||||
.upload-area:hover {
|
||||
background-color: rgba($color: #fff, $alpha: 0.1);
|
||||
}
|
||||
|
||||
.upload-area > i {
|
||||
font-size: 4em;
|
||||
margin: 10px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.upload-area > p {
|
||||
margin: 0px 10px 10px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
|
||||
import Markdown from './markdown'
|
||||
import Content from './context.vue'
|
||||
import { FileTransfer, FileListItem } from '~/neko/types'
|
||||
|
||||
@Component({
|
||||
name: 'neko-files',
|
||||
components: {
|
||||
'neko-markdown': Markdown,
|
||||
'neko-context': Content,
|
||||
},
|
||||
})
|
||||
export default class extends Vue {
|
||||
public uploadAreaDrag: boolean = false
|
||||
|
||||
get cwd() {
|
||||
return this.$accessor.files.cwd
|
||||
}
|
||||
|
||||
get files() {
|
||||
return this.$accessor.files.files
|
||||
}
|
||||
|
||||
get transfers() {
|
||||
return this.$accessor.files.transfers
|
||||
}
|
||||
|
||||
get downloads() {
|
||||
return this.$accessor.files.transfers.filter((t) => t.direction === 'download')
|
||||
}
|
||||
|
||||
get uploads() {
|
||||
return this.$accessor.files.transfers.filter((t) => t.direction === 'upload')
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.$accessor.files.refresh()
|
||||
}
|
||||
|
||||
download(item: FileListItem) {
|
||||
if (this.downloads.map((t) => t.name).includes(item.name)) {
|
||||
return
|
||||
}
|
||||
|
||||
const url =
|
||||
'/file?pwd=' + encodeURIComponent(this.$accessor.password) + '&filename=' + encodeURIComponent(item.name)
|
||||
const abortController = new AbortController()
|
||||
|
||||
let transfer: FileTransfer = {
|
||||
id: Math.round(Math.random() * 10000),
|
||||
name: item.name,
|
||||
direction: 'download',
|
||||
// this may be smaller than the actual transfer amount, but for large files the
|
||||
// content length is not sent (chunked transfer)
|
||||
size: item.size,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
abortController: abortController,
|
||||
}
|
||||
|
||||
this.$http
|
||||
.get(url, {
|
||||
responseType: 'blob',
|
||||
signal: abortController.signal,
|
||||
withCredentials: false,
|
||||
onDownloadProgress: (x) => {
|
||||
transfer.progress = x.loaded
|
||||
|
||||
if (x.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>
|
@ -31,6 +31,19 @@
|
||||
}"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="fileTransfer">
|
||||
<i
|
||||
:class="[{ disabled: !admin }, { locked: isLocked('file_transfer') }, 'fas', 'fa-file']"
|
||||
@click="toggleLock('file_transfer')"
|
||||
v-tooltip="{
|
||||
content: lockedTooltip('file_transfer'),
|
||||
placement: 'bottom',
|
||||
offset: 5,
|
||||
boundariesElement: 'body',
|
||||
delay: { show: 300, hide: 100 },
|
||||
}"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span v-if="showBadge" class="badge">•</span>
|
||||
<i class="fas fa-bars toggle" @click="toggleMenu" />
|
||||
@ -144,7 +157,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
import { AdminLockResource } from '~/neko/messages'
|
||||
|
||||
@Component({ name: 'neko-settings' })
|
||||
@ -169,26 +182,24 @@
|
||||
return !this.side && this.readTexts != this.texts
|
||||
}
|
||||
|
||||
get fileTransfer() {
|
||||
return this.$accessor.remote.fileTransfer
|
||||
}
|
||||
|
||||
toggleLock(resource: AdminLockResource) {
|
||||
this.$accessor.toggleLock(resource)
|
||||
}
|
||||
|
||||
isLocked(resource: AdminLockResource): boolean {
|
||||
return this.$accessor.isLocked(resource)
|
||||
}
|
||||
|
||||
readTexts: number = 0
|
||||
toggleMenu() {
|
||||
this.$accessor.client.toggleSide()
|
||||
this.readTexts = this.texts
|
||||
}
|
||||
|
||||
toggleLock(resource: AdminLockResource) {
|
||||
if (!this.admin) return
|
||||
|
||||
if (this.isLocked(resource)) {
|
||||
this.$accessor.unlock(resource)
|
||||
} else {
|
||||
this.$accessor.lock(resource)
|
||||
}
|
||||
}
|
||||
|
||||
isLocked(resource: AdminLockResource): boolean {
|
||||
return resource in this.locked && this.locked[resource]
|
||||
}
|
||||
|
||||
lockedTooltip(resource: AdminLockResource) {
|
||||
if (this.admin) {
|
||||
return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `unlock` : `lock`))
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 } =
|
||||
defaultRules
|
||||
|
@ -157,8 +157,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||
import { Member } from '~/neko/types'
|
||||
import { Component, Ref, Vue } from 'vue-property-decorator'
|
||||
|
||||
import Content from './context.vue'
|
||||
import Avatar from './avatar.vue'
|
||||
|
@ -60,7 +60,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
import { messages } from '~/locale'
|
||||
|
||||
@Component({ name: 'neko-menu' })
|
||||
|
@ -97,7 +97,7 @@
|
||||
</style>
|
||||
|
||||
<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'
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -304,7 +304,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||
import { Component, Watch, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component({ name: 'neko-settings' })
|
||||
export default class extends Vue {
|
||||
|
@ -6,6 +6,10 @@
|
||||
<i class="fas fa-comment-alt" />
|
||||
<span>{{ $t('side.chat') }}</span>
|
||||
</li>
|
||||
<li v-if="filetransferAllowed" :class="{ active: tab === 'files' }" @click.stop.prevent="change('files')">
|
||||
<i class="fas fa-file" />
|
||||
<span>{{ $t('side.files') }}</span>
|
||||
</li>
|
||||
<li :class="{ active: tab === 'settings' }" @click.stop.prevent="change('settings')">
|
||||
<i class="fas fa-sliders-h" />
|
||||
<span>{{ $t('side.settings') }}</span>
|
||||
@ -14,6 +18,7 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<neko-chat v-if="tab === 'chat'" />
|
||||
<neko-files v-if="tab === 'files'" />
|
||||
<neko-settings v-if="tab === 'settings'" />
|
||||
</div>
|
||||
</aside>
|
||||
@ -74,23 +79,47 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'vue-property-decorator'
|
||||
import { Vue, Component, Watch } from 'vue-property-decorator'
|
||||
|
||||
import Settings from '~/components/settings.vue'
|
||||
import Chat from '~/components/chat.vue'
|
||||
import Files from '~/components/files.vue'
|
||||
|
||||
@Component({
|
||||
name: 'neko',
|
||||
components: {
|
||||
'neko-settings': Settings,
|
||||
'neko-chat': Chat,
|
||||
'neko-files': Files,
|
||||
},
|
||||
})
|
||||
export default class extends Vue {
|
||||
get filetransferAllowed() {
|
||||
return (
|
||||
this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer'))
|
||||
)
|
||||
}
|
||||
|
||||
get tab() {
|
||||
return this.$accessor.client.tab
|
||||
}
|
||||
|
||||
@Watch('tab', { immediate: true })
|
||||
@Watch('filetransferAllowed', { immediate: true })
|
||||
onTabChange() {
|
||||
// do not show the files tab if file transfer is disabled
|
||||
if (this.tab === 'files' && !this.filetransferAllowed) {
|
||||
this.change('chat')
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('filetransferAllowed')
|
||||
onFileTransferAllowedChange() {
|
||||
if (this.filetransferAllowed) {
|
||||
this.$accessor.files.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
change(tab: string) {
|
||||
this.$accessor.client.setTab(tab)
|
||||
}
|
||||
|
@ -68,7 +68,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component({ name: 'neko-unsupported' })
|
||||
export default class extends Vue {}
|
||||
|
@ -21,11 +21,14 @@
|
||||
@mouseup.stop.prevent="onMouseUp"
|
||||
@mouseenter.stop.prevent="onMouseEnter"
|
||||
@mouseleave.stop.prevent="onMouseLeave"
|
||||
@touchmove.stop.prevent="onTouchHandler"
|
||||
@touchstart.stop.prevent="onTouchHandler"
|
||||
@touchend.stop.prevent="onTouchHandler"
|
||||
/>
|
||||
<div v-if="!playing && playable" class="player-overlay" @click.stop.prevent="toggle">
|
||||
<div v-if="!playing && playable" class="player-overlay" @click.stop.prevent="playAndUnmute">
|
||||
<i class="fas fa-play-circle" />
|
||||
</div>
|
||||
<div v-if="mutedOverlay && muted" class="player-overlay" @click.stop.prevent="unmute">
|
||||
<div v-else-if="mutedOverlay && muted" class="player-overlay" @click.stop.prevent="unmute">
|
||||
<i class="fas fa-volume-up" />
|
||||
</div>
|
||||
<div ref="aspect" class="player-aspect" />
|
||||
@ -33,7 +36,7 @@
|
||||
<ul v-if="!fullscreen && !hideControls" class="video-menu top">
|
||||
<li><i @click.stop.prevent="requestFullscreen" class="fas fa-expand"></i></li>
|
||||
<li v-if="admin"><i @click.stop.prevent="openResolution" class="fas fa-desktop"></i></li>
|
||||
<li class="request-control">
|
||||
<li v-if="!implicitHosting" :class="extraControls || 'extra-control'">
|
||||
<i
|
||||
:class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']"
|
||||
@click.stop.prevent="toggleControl"
|
||||
@ -106,12 +109,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.request-control {
|
||||
/* usually extra controls are only shown on mobile */
|
||||
&.extra-control {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
&.request-control {
|
||||
&.extra-control {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@ -194,7 +197,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue, Prop } from 'vue-property-decorator'
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
import { elementRequestFullscreen, onFullscreenChange, isFullscreen } from '~/utils'
|
||||
import { elementRequestFullscreen, onFullscreenChange, isFullscreen, lockKeyboard, unlockKeyboard } from '~/utils'
|
||||
|
||||
import Emote from './emote.vue'
|
||||
import Resolution from './resolution.vue'
|
||||
@ -223,13 +226,15 @@
|
||||
@Ref('resolution') readonly _resolution!: Resolution
|
||||
@Ref('clipboard') readonly _clipboard!: Clipboard
|
||||
|
||||
// all controls are hidden (e.g. for cast mode)
|
||||
@Prop(Boolean) readonly hideControls!: boolean
|
||||
// extra controls are shown (e.g. for embed mode)
|
||||
@Prop(Boolean) readonly extraControls!: boolean
|
||||
|
||||
private keyboard = GuacamoleKeyboard()
|
||||
private observer = new ResizeObserver(this.onResize.bind(this))
|
||||
private focused = false
|
||||
private fullscreen = false
|
||||
private startsMuted = true
|
||||
private mutedOverlay = true
|
||||
|
||||
get admin() {
|
||||
@ -339,12 +344,12 @@
|
||||
}
|
||||
|
||||
@Watch('width')
|
||||
onWidthChanged(width: number) {
|
||||
onWidthChanged() {
|
||||
this.onResize()
|
||||
}
|
||||
|
||||
@Watch('height')
|
||||
onHeightChanged(height: number) {
|
||||
onHeightChanged() {
|
||||
this.onResize()
|
||||
}
|
||||
|
||||
@ -361,7 +366,6 @@
|
||||
onMutedChanged(muted: boolean) {
|
||||
if (this._video && this._video.muted != muted) {
|
||||
this._video.muted = muted
|
||||
this.startsMuted = muted
|
||||
|
||||
if (!muted) {
|
||||
this.mutedOverlay = false
|
||||
@ -384,9 +388,29 @@
|
||||
}
|
||||
|
||||
@Watch('playing')
|
||||
onPlayingChanged(playing: boolean) {
|
||||
async onPlayingChanged(playing: boolean) {
|
||||
if (this._video && this._video.paused && playing) {
|
||||
this.play()
|
||||
// if autoplay is disabled, play() will throw an error
|
||||
// and we need to properly save the state otherwise we
|
||||
// would be thinking we're playing when we're not
|
||||
try {
|
||||
await this._video.play()
|
||||
} catch (err: any) {
|
||||
if (!this._video.muted) {
|
||||
// video.play() can fail if audio is set due restrictive
|
||||
// browsers autoplay policy -> retry with muted audio
|
||||
try {
|
||||
this.$accessor.video.setMuted(true)
|
||||
this._video.muted = true
|
||||
await this._video.play()
|
||||
} catch (err: any) {
|
||||
// if it still fails, we're not playing anything
|
||||
this.$accessor.video.pause()
|
||||
}
|
||||
} else {
|
||||
this.$accessor.video.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._video && !this._video.paused && !playing) {
|
||||
@ -417,17 +441,13 @@
|
||||
|
||||
onFullscreenChange(this._player, () => {
|
||||
this.fullscreen = isFullscreen()
|
||||
this.fullscreen ? lockKeyboard() : unlockKeyboard()
|
||||
this.onResize()
|
||||
})
|
||||
|
||||
this._video.addEventListener('canplaythrough', () => {
|
||||
this.$accessor.video.setPlayable(true)
|
||||
if (this.autoplay) {
|
||||
// start as muted due to restrictive browsers autoplay policy
|
||||
if (this.startsMuted && (!document.hasFocus() || !this.$accessor.active)) {
|
||||
this.$accessor.video.setMuted(true)
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$accessor.video.play()
|
||||
})
|
||||
@ -443,7 +463,7 @@
|
||||
this.$accessor.video.setPlayable(false)
|
||||
})
|
||||
|
||||
this._video.addEventListener('volumechange', (event) => {
|
||||
this._video.addEventListener('volumechange', () => {
|
||||
this.$accessor.video.setMuted(this._video.muted)
|
||||
this.$accessor.video.setVolume(this._video.volume * 100)
|
||||
})
|
||||
@ -559,6 +579,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
playAndUnmute() {
|
||||
this.$accessor.video.play()
|
||||
this.$accessor.video.setMuted(false)
|
||||
}
|
||||
|
||||
unmute() {
|
||||
this.$accessor.video.setMuted(false)
|
||||
}
|
||||
@ -666,6 +691,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
onTouchHandler(e: TouchEvent) {
|
||||
let first = e.changedTouches[0]
|
||||
let type = ''
|
||||
switch (e.type) {
|
||||
case 'touchstart':
|
||||
type = 'mousedown'
|
||||
break
|
||||
case 'touchmove':
|
||||
type = 'mousemove'
|
||||
break
|
||||
case 'touchend':
|
||||
type = 'mouseup'
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
const simulatedEvent = new MouseEvent(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
screenX: first.screenX,
|
||||
screenY: first.screenY,
|
||||
clientX: first.clientX,
|
||||
clientY: first.clientY,
|
||||
})
|
||||
first.target.dispatchEvent(simulatedEvent)
|
||||
}
|
||||
|
||||
onMouseDown(e: MouseEvent) {
|
||||
if (!this.hosting) {
|
||||
this.$emit('control-attempt', e)
|
||||
|
@ -38,9 +38,9 @@ const exportMixin = {
|
||||
$accessor() {
|
||||
return neko
|
||||
},
|
||||
$client () {
|
||||
$client() {
|
||||
return window.$client
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -52,15 +52,8 @@ const plugini18n: PluginObject<undefined> = {
|
||||
},
|
||||
}
|
||||
|
||||
function extend (component: any) {
|
||||
return component
|
||||
.use(plugini18n)
|
||||
.use(Logger)
|
||||
.use(Axios)
|
||||
.use(Swal)
|
||||
.use(Anime)
|
||||
.use(Client)
|
||||
.extend(exportMixin)
|
||||
function extend(component: any) {
|
||||
return component.use(plugini18n).use(Logger).use(Axios).use(Swal).use(Anime).use(Client).extend(exportMixin)
|
||||
}
|
||||
|
||||
export const NekoConnect = extend(Connect)
|
||||
|
@ -7,6 +7,7 @@ export const send_a_message = 'Sende eine Nachricht'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Dateien',
|
||||
settings: 'Einstellungen',
|
||||
}
|
||||
|
||||
@ -68,6 +69,14 @@ export const locks = {
|
||||
notif_locked: 'Raum gesperrt',
|
||||
notif_unlocked: 'Raum entsperrt',
|
||||
},
|
||||
file_transfer: {
|
||||
lock: 'Dateiübertragung sperren (für Nutzer)',
|
||||
unlock: 'Dateiübertragung entsperren (für Nutzer)',
|
||||
locked: 'Dateiübertragung gesperrt (für Nutzer)',
|
||||
unlocked: 'Dateiübertragung entsperrt (für Nutzer)',
|
||||
notif_locked: 'Dateiübertragung gesperrt',
|
||||
notif_unlocked: 'Dateiübertragung entsperrt',
|
||||
},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -108,3 +117,9 @@ export const notifications = {
|
||||
muted: '{name} stummgeschaltet',
|
||||
unmuted: '{name} stummschaltung aufgehoben',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Herunterladen',
|
||||
uploads: 'Hochladen',
|
||||
upload_here: 'Klicken oder ziehen Sie Dateien zum Hochladen hierher',
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export const send_a_message = 'Send a message'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Files',
|
||||
settings: 'Settings',
|
||||
}
|
||||
|
||||
@ -70,6 +71,14 @@ export const locks = {
|
||||
notif_locked: 'locked the room',
|
||||
notif_unlocked: 'unlocked the room',
|
||||
},
|
||||
file_transfer: {
|
||||
lock: 'Lock File Transfer (for users)',
|
||||
unlock: 'Unlock File Transfer (for users)',
|
||||
locked: 'File Transfer Locked (for users)',
|
||||
unlocked: 'File Transfer Unlocked (for users)',
|
||||
notif_locked: 'locked file transfer',
|
||||
notif_unlocked: 'unlocked file transfer',
|
||||
},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -110,3 +119,9 @@ export const notifications = {
|
||||
muted: 'muted {name}',
|
||||
unmuted: 'unmuted {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Downloads',
|
||||
uploads: 'Uploads',
|
||||
upload_here: 'Click or drag files here to upload',
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export const send_a_message = 'Enviar un mensaje'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Archivos',
|
||||
settings: 'Configuración',
|
||||
}
|
||||
|
||||
@ -74,6 +75,15 @@ export const locks = {
|
||||
notif_locked: 'bloqueó la sala',
|
||||
notif_unlocked: 'desbloqueó la sala',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -117,3 +127,9 @@ export const notifications = {
|
||||
muted: '{name} silenciado',
|
||||
unmuted: '{name} no silenciado',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Descargas',
|
||||
uploads: 'Cargar',
|
||||
upload_here: 'Haga clic o arrastre los archivos aquí para cargarlos',
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export const send_a_message = 'Lähetä viesti'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chatti',
|
||||
files: 'Tiedostot',
|
||||
settings: 'Asetukset',
|
||||
}
|
||||
|
||||
@ -70,6 +71,15 @@ export const locks = {
|
||||
notif_locked: 'lukittu huone',
|
||||
notif_unlocked: 'vapautettu huone',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -110,3 +120,9 @@ export const notifications = {
|
||||
muted: 'mykistetty {name}',
|
||||
unmuted: 'poistettu mykistys {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Lataukset',
|
||||
uploads: 'Lataa',
|
||||
upload_here: 'Klikkaa tai vedä tiedostoja tähän ladataksesi',
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export const send_a_message = 'Envoyer un message'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Fichiers',
|
||||
settings: 'Paramètres',
|
||||
}
|
||||
|
||||
@ -74,6 +75,15 @@ export const locks = {
|
||||
notif_locked: 'a vérouillé la salle',
|
||||
notif_unlocked: 'a dévérouillé la salle',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -117,3 +127,9 @@ export const notifications = {
|
||||
muted: 'a mute {name}',
|
||||
unmuted: 'a démute {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Téléchargements',
|
||||
uploads: 'Télécharger',
|
||||
upload_here: 'Cliquez ou faites glisser les fichiers ici pour les télécharger',
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export const send_a_message = '메세지 보내기'
|
||||
|
||||
export const side = {
|
||||
chat: '채팅',
|
||||
files: '파일',
|
||||
settings: '설정',
|
||||
}
|
||||
|
||||
@ -68,6 +69,15 @@ export const locks = {
|
||||
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 = {
|
||||
@ -108,3 +118,9 @@ export const notifications = {
|
||||
muted: '{name} 님이 뮤트됐습니다',
|
||||
unmuted: '{name} 님의 뮤트가 해제됐습니다',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: '다운로드',
|
||||
uploads: '업로드',
|
||||
upload_here: '업로드할 파일을 여기로 클릭하거나 드래그하세요.',
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export const send_a_message = 'Send en melding'
|
||||
|
||||
export const side = {
|
||||
chat: 'Sludring',
|
||||
files: 'Filer',
|
||||
settings: 'Innstillinger',
|
||||
}
|
||||
|
||||
@ -74,6 +75,15 @@ export const locks = {
|
||||
notif_locked: 'låste rommet',
|
||||
notif_unlocked: 'låste opp rommet',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -117,3 +127,9 @@ export const notifications = {
|
||||
muted: 'forstummet {name}',
|
||||
unmuted: 'opphevet forstummingen av {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Overførsler',
|
||||
uploads: 'Overfør',
|
||||
upload_here: 'Klik eller træk filer her for at uploade',
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export const send_a_message = 'Отправить сообщение'
|
||||
|
||||
export const side = {
|
||||
chat: 'Чат',
|
||||
files: 'Файлы',
|
||||
settings: 'Настройки',
|
||||
}
|
||||
|
||||
@ -70,6 +71,15 @@ export const locks = {
|
||||
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 = {
|
||||
@ -110,3 +120,9 @@ export const notifications = {
|
||||
muted: 'заглушен {name}',
|
||||
unmuted: 'не заглушен {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Загрузки',
|
||||
uploads: 'Загрузить',
|
||||
upload_here: 'Нажмите или перетащите сюда файлы для загрузки',
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export const send_a_message = 'Odoslať správu'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chat',
|
||||
files: 'Súbory',
|
||||
settings: 'Nastavenia',
|
||||
}
|
||||
|
||||
@ -73,6 +74,15 @@ export const locks = {
|
||||
notif_locked: 'miestnosť bola zamknutá',
|
||||
notif_unlocked: 'miestnosť bola odomknutá',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -113,3 +123,9 @@ export const notifications = {
|
||||
muted: 'zakázal chat používateľovi {name}',
|
||||
unmuted: 'povolil chat používateľovi {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Stiahnutia',
|
||||
uploads: 'Nahrávanie',
|
||||
upload_here: 'Kliknutím alebo pretiahnutím súborov sem ich môžete nahrať',
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export const send_a_message = 'Skicka ett meddelande'
|
||||
|
||||
export const side = {
|
||||
chat: 'Chatt',
|
||||
files: 'Filer',
|
||||
settings: 'Inställningar',
|
||||
}
|
||||
|
||||
@ -74,6 +75,15 @@ export const locks = {
|
||||
notif_locked: 'låste rummet',
|
||||
notif_unlocked: 'låste upp rummet',
|
||||
},
|
||||
// TODO
|
||||
//file_transfer: {
|
||||
// lock: 'Lock File Transfer (for users)',
|
||||
// unlock: 'Unlock File Transfer (for users)',
|
||||
// locked: 'File Transfer Locked (for users)',
|
||||
// unlocked: 'File Transfer Unlocked (for users)',
|
||||
// notif_locked: 'locked file transfer',
|
||||
// notif_unlocked: 'unlocked file transfer',
|
||||
//},
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
@ -117,3 +127,9 @@ export const notifications = {
|
||||
muted: 'tystade {name}',
|
||||
unmuted: 'tog bort tystningen på {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: 'Nedladdningar',
|
||||
uploads: 'Ladda upp',
|
||||
upload_here: 'Klicka eller dra filer hit för att ladda upp dem',
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export const send_a_message = '发送消息'
|
||||
|
||||
export const side = {
|
||||
chat: '聊天',
|
||||
files: '文件',
|
||||
settings: '设置',
|
||||
}
|
||||
|
||||
@ -70,6 +71,15 @@ export const locks = {
|
||||
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 = {
|
||||
@ -110,3 +120,9 @@ export const notifications = {
|
||||
muted: '鸟粪 {name}',
|
||||
unmuted: '取消静音 {name}',
|
||||
}
|
||||
|
||||
export const files = {
|
||||
downloads: '下载',
|
||||
uploads: '上传',
|
||||
upload_here: '点击或拖动文件到这里来上传',
|
||||
}
|
||||
|
@ -66,8 +66,8 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
this._ws = new WebSocket(`${url}?password=${encodeURIComponent(password)}`)
|
||||
this.emit('debug', `connecting to ${this._ws.url}`)
|
||||
this._ws.onmessage = this.onMessage.bind(this)
|
||||
this._ws.onerror = (event) => this.onError.bind(this)
|
||||
this._ws.onclose = (event) => this.onDisconnected.bind(this, new Error('websocket closed'))
|
||||
this._ws.onerror = () => this.onError.bind(this)
|
||||
this._ws.onclose = () => this.onDisconnected.bind(this, new Error('websocket closed'))
|
||||
this._timeout = window.setTimeout(this.onTimeout.bind(this), 15000)
|
||||
} catch (err: any) {
|
||||
this.onDisconnected(err)
|
||||
@ -203,22 +203,23 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
return
|
||||
}
|
||||
|
||||
this._peer = new RTCPeerConnection()
|
||||
if (lite !== true) {
|
||||
this._peer = new RTCPeerConnection({
|
||||
iceServers: servers,
|
||||
})
|
||||
} else {
|
||||
this._peer = new RTCPeerConnection()
|
||||
}
|
||||
|
||||
this._peer.onconnectionstatechange = (event) => {
|
||||
this._peer.onconnectionstatechange = () => {
|
||||
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._peer.oniceconnectionstatechange = (event) => {
|
||||
this._peer.oniceconnectionstatechange = () => {
|
||||
this._state = this._peer!.iceConnectionState
|
||||
|
||||
this.emit('debug', `peer ice connection state changed: ${this._peer!.iceConnectionState}`)
|
||||
@ -251,11 +252,28 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
|
||||
this._peer.ontrack = this.onTrack.bind(this)
|
||||
|
||||
this._peer.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||
if (!event.candidate) {
|
||||
this.emit('debug', `sent all local ICE candidates`)
|
||||
return
|
||||
}
|
||||
|
||||
const init = event.candidate.toJSON()
|
||||
this.emit('debug', `sending local ICE candidate`, init)
|
||||
|
||||
this._ws!.send(
|
||||
JSON.stringify({
|
||||
event: EVENT.SIGNAL.CANDIDATE,
|
||||
data: JSON.stringify(init),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
this._peer.onnegotiationneeded = async () => {
|
||||
this.emit('warn', `negotiation is needed`)
|
||||
|
||||
const d = await this._peer!.createOffer()
|
||||
this._peer!.setLocalDescription(d)
|
||||
await this._peer!.setLocalDescription(d)
|
||||
|
||||
this._ws!.send(
|
||||
JSON.stringify({
|
||||
@ -277,15 +295,19 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
return
|
||||
}
|
||||
|
||||
this._peer.setRemoteDescription({ type: 'offer', sdp })
|
||||
await this._peer.setRemoteDescription({ type: 'offer', sdp })
|
||||
|
||||
for (const candidate of this._candidates) {
|
||||
this._peer.addIceCandidate(candidate)
|
||||
await this._peer.addIceCandidate(candidate)
|
||||
}
|
||||
this._candidates = []
|
||||
|
||||
try {
|
||||
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._ws!.send(
|
||||
@ -306,7 +328,7 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
return
|
||||
}
|
||||
|
||||
this._peer.setRemoteDescription({ type: 'answer', sdp })
|
||||
await this._peer.setRemoteDescription({ type: 'answer', sdp })
|
||||
}
|
||||
|
||||
private async onMessage(e: MessageEvent) {
|
||||
|
@ -38,6 +38,10 @@ export const EVENT = {
|
||||
MESSAGE: 'chat/message',
|
||||
EMOTE: 'chat/emote',
|
||||
},
|
||||
FILETRANSFER: {
|
||||
LIST: 'filetransfer/list',
|
||||
REFRESH: 'filetransfer/refresh',
|
||||
},
|
||||
SCREEN: {
|
||||
CONFIGURATIONS: 'screen/configurations',
|
||||
RESOLUTION: 'screen/resolution',
|
||||
@ -69,6 +73,7 @@ export type WebSocketEvents =
|
||||
| MemberEvents
|
||||
| SignalEvents
|
||||
| ChatEvents
|
||||
| FileTransferEvents
|
||||
| ScreenEvents
|
||||
| BroadcastEvents
|
||||
| AdminEvents
|
||||
@ -91,6 +96,9 @@ export type SignalEvents =
|
||||
| typeof EVENT.SIGNAL.CANDIDATE
|
||||
|
||||
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
|
||||
|
||||
export type FileTransferEvents = typeof EVENT.FILETRANSFER.LIST | typeof EVENT.FILETRANSFER.REFRESH
|
||||
|
||||
export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET
|
||||
|
||||
export type BroadcastEvents =
|
||||
|
@ -7,7 +7,6 @@ import { accessor } from '~/store'
|
||||
|
||||
import {
|
||||
SystemMessagePayload,
|
||||
SignalProvidePayload,
|
||||
MemberListPayload,
|
||||
MemberDisconnectPayload,
|
||||
MemberPayload,
|
||||
@ -19,11 +18,11 @@ import {
|
||||
ScreenConfigurationsPayload,
|
||||
ScreenResolutionPayload,
|
||||
BroadcastStatusPayload,
|
||||
AdminPayload,
|
||||
AdminTargetPayload,
|
||||
AdminLockMessage,
|
||||
SystemInitPayload,
|
||||
AdminLockResource,
|
||||
FileTransferListPayload,
|
||||
} from './messages'
|
||||
|
||||
interface NekoEvents extends BaseEvents {}
|
||||
@ -46,6 +45,8 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
||||
this.$vue = vue
|
||||
this.$accessor = vue.$accessor
|
||||
this.url = url
|
||||
// convert ws url to http url
|
||||
this.$vue.$http.defaults.baseURL = url.replace(/^ws/, 'http').replace(/\/ws$/, '')
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
@ -128,13 +129,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
||||
this.$accessor.video.setStream(0)
|
||||
}
|
||||
|
||||
protected [EVENT.DATA](data: any) {}
|
||||
protected [EVENT.DATA]() {}
|
||||
|
||||
/////////////////////////////
|
||||
// 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.setFileTransfer(file_transfer)
|
||||
|
||||
for (const resource in locks) {
|
||||
this[EVENT.ADMIN.LOCK]({
|
||||
@ -351,6 +353,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
||||
this.$accessor.chat.newEmote({ type: emote })
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// File Transfer Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.FILETRANSFER.LIST]({ cwd, files }: FileTransferListPayload) {
|
||||
this.$accessor.files.setCwd(cwd)
|
||||
this.$accessor.files.setFileList(files)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Screen Events
|
||||
/////////////////////////////
|
||||
|
@ -8,8 +8,9 @@ import {
|
||||
ChatEvents,
|
||||
ScreenEvents,
|
||||
AdminEvents,
|
||||
FileTransferEvents,
|
||||
} from './events'
|
||||
import { Member, ScreenConfigurations, ScreenResolution } from './types'
|
||||
import { FileListItem, Member, ScreenConfigurations, ScreenResolution } from './types'
|
||||
|
||||
export type WebSocketMessages =
|
||||
| WebSocketMessage
|
||||
@ -59,6 +60,7 @@ export interface SystemInit extends WebSocketMessage, SystemInitPayload {
|
||||
export interface SystemInitPayload {
|
||||
implicit_hosting: boolean
|
||||
locks: Record<string, string>
|
||||
file_transfer: boolean
|
||||
}
|
||||
|
||||
// system/disconnect
|
||||
@ -192,6 +194,18 @@ export interface EmojiSendPayload {
|
||||
emote: string
|
||||
}
|
||||
|
||||
/*
|
||||
FILE TRANSFER PAYLOADS
|
||||
*/
|
||||
export interface FileTransferListMessage extends WebSocketMessage, FileTransferListPayload {
|
||||
event: FileTransferEvents
|
||||
}
|
||||
|
||||
export interface FileTransferListPayload {
|
||||
cwd: string
|
||||
files: FileListItem[]
|
||||
}
|
||||
|
||||
/*
|
||||
SCREEN PAYLOADS
|
||||
*/
|
||||
@ -215,11 +229,11 @@ export interface ScreenConfigurationsPayload {
|
||||
BROADCAST PAYLOADS
|
||||
*/
|
||||
export interface BroadcastCreatePayload {
|
||||
url: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface BroadcastStatusPayload {
|
||||
url: string
|
||||
url: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
@ -248,7 +262,7 @@ export interface AdminLockMessage extends WebSocketMessage, AdminLockPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type AdminLockResource = 'login' | 'control'
|
||||
export type AdminLockResource = 'login' | 'control' | 'file_transfer'
|
||||
|
||||
export interface AdminLockPayload {
|
||||
resource: AdminLockResource
|
||||
|
@ -22,3 +22,20 @@ export interface ScreenResolution {
|
||||
height: number
|
||||
rate: number
|
||||
}
|
||||
|
||||
export interface FileListItem {
|
||||
name: string
|
||||
type: 'file' | 'dir'
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface FileTransfer {
|
||||
id: number
|
||||
name: string
|
||||
direction: 'upload' | 'download'
|
||||
size: number
|
||||
progress: number
|
||||
status: 'pending' | 'inprogress' | 'completed' | 'failed'
|
||||
error?: string
|
||||
abortController?: AbortController
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ declare module 'vue/types/vue' {
|
||||
$swal: VueSwalInstance
|
||||
}
|
||||
|
||||
interface VueConstructor<V extends Vue = Vue> {
|
||||
interface VueConstructor {
|
||||
swal: VueSwalInstance
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export const actions = actionTree(
|
||||
accessor.chat.addEmote({ id, emote })
|
||||
},
|
||||
|
||||
newMessage({ state }, message: Message) {
|
||||
newMessage(store, message: Message) {
|
||||
if (accessor.settings.chat_sound) {
|
||||
new Audio('chat.mp3').play().catch(console.error)
|
||||
}
|
||||
|
72
client/src/store/files.ts
Normal file
72
client/src/store/files.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { actionTree, getterTree, mutationTree } from 'typed-vuex'
|
||||
import { FileListItem, FileTransfer } from '~/neko/types'
|
||||
import { EVENT } from '~/neko/events'
|
||||
import { accessor } from '~/store'
|
||||
|
||||
export const state = () => ({
|
||||
cwd: '',
|
||||
files: [] as FileListItem[],
|
||||
transfers: [] as FileTransfer[],
|
||||
})
|
||||
|
||||
export const getters = getterTree(state, {
|
||||
//
|
||||
})
|
||||
|
||||
export const mutations = mutationTree(state, {
|
||||
_setCwd(state, cwd: string) {
|
||||
state.cwd = cwd
|
||||
},
|
||||
|
||||
_setFileList(state, files: FileListItem[]) {
|
||||
state.files = files
|
||||
},
|
||||
|
||||
_addTransfer(state, transfer: FileTransfer) {
|
||||
state.transfers = [...state.transfers, transfer]
|
||||
},
|
||||
|
||||
_removeTransfer(state, transfer: FileTransfer) {
|
||||
state.transfers = state.transfers.filter((t) => t.id !== transfer.id)
|
||||
},
|
||||
})
|
||||
|
||||
export const actions = actionTree(
|
||||
{ state, getters, mutations },
|
||||
{
|
||||
setCwd(store, cwd: string) {
|
||||
accessor.files._setCwd(cwd)
|
||||
},
|
||||
|
||||
setFileList(store, files: FileListItem[]) {
|
||||
accessor.files._setFileList(files)
|
||||
},
|
||||
|
||||
addTransfer(store, transfer: FileTransfer) {
|
||||
if (transfer.status !== 'pending') {
|
||||
return
|
||||
}
|
||||
accessor.files._addTransfer(transfer)
|
||||
},
|
||||
|
||||
removeTransfer(store, transfer: FileTransfer) {
|
||||
accessor.files._removeTransfer(transfer)
|
||||
},
|
||||
|
||||
cancelAllTransfers() {
|
||||
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)
|
||||
},
|
||||
},
|
||||
)
|
@ -1,12 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { useAccessor, mutationTree, actionTree } from 'typed-vuex'
|
||||
import { useAccessor, mutationTree, getterTree, actionTree } from 'typed-vuex'
|
||||
import { EVENT } from '~/neko/events'
|
||||
import { AdminLockResource } from '~/neko/messages'
|
||||
import { get, set } from '~/utils/localstorage'
|
||||
|
||||
import * as video from './video'
|
||||
import * as chat from './chat'
|
||||
import * as files from './files'
|
||||
import * as remote from './remote'
|
||||
import * as user from './user'
|
||||
import * as settings from './settings'
|
||||
@ -55,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(
|
||||
{ state, mutations },
|
||||
{ state, getters, mutations },
|
||||
{
|
||||
initialise(store) {
|
||||
initialise() {
|
||||
accessor.emoji.initialise()
|
||||
accessor.settings.initialise()
|
||||
},
|
||||
@ -79,12 +84,20 @@ export const actions = actionTree(
|
||||
$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 })
|
||||
$client.login(password, displayname)
|
||||
},
|
||||
|
||||
logout({ state }) {
|
||||
logout() {
|
||||
accessor.setLogin({ displayname: '', password: '' })
|
||||
set('displayname', '')
|
||||
set('password', '')
|
||||
@ -97,7 +110,8 @@ export const storePattern = {
|
||||
state,
|
||||
mutations,
|
||||
actions,
|
||||
modules: { video, chat, user, remote, settings, client, emoji },
|
||||
getters,
|
||||
modules: { video, chat, files, user, remote, settings, client, emoji },
|
||||
}
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
@ -13,6 +13,7 @@ export const state = () => ({
|
||||
clipboard: '',
|
||||
locked: false,
|
||||
implicitHosting: true,
|
||||
fileTransfer: true,
|
||||
keyboardModifierState: -1,
|
||||
})
|
||||
|
||||
@ -20,7 +21,7 @@ export const getters = getterTree(state, {
|
||||
hosting: (state, getters, root) => {
|
||||
return root.user.id === state.id || state.implicitHosting
|
||||
},
|
||||
hosted: (state, getters, root) => {
|
||||
hosted: (state) => {
|
||||
return state.id !== '' || state.implicitHosting
|
||||
},
|
||||
host: (state, getters, root) => {
|
||||
@ -53,6 +54,10 @@ export const mutations = mutationTree(state, {
|
||||
state.implicitHosting = val
|
||||
},
|
||||
|
||||
setFileTransfer(state, val: boolean) {
|
||||
state.fileTransfer = val
|
||||
},
|
||||
|
||||
reset(state) {
|
||||
state.id = ''
|
||||
state.clipboard = ''
|
||||
@ -131,7 +136,7 @@ export const actions = actionTree(
|
||||
$client.sendMessage(EVENT.ADMIN.RELEASE)
|
||||
},
|
||||
|
||||
adminGive({ getters }, member: string | Member) {
|
||||
adminGive(store, member: string | Member) {
|
||||
if (!accessor.connected) {
|
||||
return
|
||||
}
|
||||
@ -155,7 +160,7 @@ export const actions = actionTree(
|
||||
$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)) {
|
||||
return
|
||||
}
|
||||
|
@ -79,13 +79,13 @@ export const actions = actionTree(
|
||||
}
|
||||
},
|
||||
|
||||
broadcastStatus({ getters }, { url, isActive }) {
|
||||
broadcastStatus(store, { url, isActive }) {
|
||||
accessor.settings.setBroadcastStatus({ url, isActive })
|
||||
},
|
||||
broadcastCreate({ getters }, url: string) {
|
||||
broadcastCreate(store, url: string) {
|
||||
$client.sendMessage(EVENT.BROADCAST.CREATE, { url })
|
||||
},
|
||||
broadcastDestroy({ getters }) {
|
||||
broadcastDestroy() {
|
||||
$client.sendMessage(EVENT.BROADCAST.DESTROY)
|
||||
},
|
||||
},
|
||||
|
@ -169,7 +169,7 @@ export const mutations = mutationTree(state, {
|
||||
export const actions = actionTree(
|
||||
{ state, getters, mutations },
|
||||
{
|
||||
screenConfiguations({ state }) {
|
||||
screenConfiguations() {
|
||||
if (!accessor.connected || !accessor.user.admin) {
|
||||
return
|
||||
}
|
||||
@ -177,7 +177,7 @@ export const actions = actionTree(
|
||||
$client.sendMessage(EVENT.SCREEN.CONFIGURATIONS)
|
||||
},
|
||||
|
||||
screenGet({ state }) {
|
||||
screenGet() {
|
||||
if (!accessor.connected) {
|
||||
return
|
||||
}
|
||||
@ -185,7 +185,7 @@ export const actions = actionTree(
|
||||
$client.sendMessage(EVENT.SCREEN.RESOLUTION)
|
||||
},
|
||||
|
||||
screenSet({ state }, resolution: ScreenResolution) {
|
||||
screenSet(store, resolution: ScreenResolution) {
|
||||
if (!accessor.connected || !accessor.user.admin) {
|
||||
return
|
||||
}
|
||||
|
15
client/src/types/navigator.keyboard.d.ts
vendored
Normal file
15
client/src/types/navigator.keyboard.d.ts
vendored
Normal 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 {}
|
@ -8,6 +8,18 @@ export function makeid(length: number) {
|
||||
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) {
|
||||
if (typeof el.requestFullscreen === 'function') {
|
||||
el.requestFullscreen()
|
||||
|
@ -22,6 +22,6 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
devServer: {
|
||||
disableHostCheck: true,
|
||||
}
|
||||
allowedHosts: 'all',
|
||||
},
|
||||
}
|
||||
|
@ -1,29 +1,44 @@
|
||||
<div align="center">
|
||||
<a href="https://neko.m1k1o.net/#/" ><img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="450" height="auto"/></a>
|
||||
<a href="https://neko.m1k1o.net/#/" ><img src="./_media/logo.png" width="450" height="auto"/></a>
|
||||
<br/>
|
||||
<br/>
|
||||
<img src="https://i.imgur.com/ZSzbQr7.gif" width="650" height="auto"/>
|
||||
<img src="./_media/intro.gif" width="650" height="auto"/>
|
||||
|
||||
With `NEKO_FILE_TRANSFER_ENABLED=true`:
|
||||
|
||||
<img src="./_media/file-transfer.gif" width="650" height="auto"/>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
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 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 multiple 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
|
||||
|
||||
* Text Chat (With basic markdown support, discord flavor)
|
||||
* Admin users (Kick, Ban & Force Give/Release Controls)
|
||||
* Admin users (Kick, Ban & Force Give/Release Controls, Lock room)
|
||||
* Clipboard synchronization (on [supported browsers](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText))
|
||||
* Emote overlay
|
||||
* Ignore user (chat and emotes)
|
||||
* Persistent settings
|
||||
* Automatic Login with custom url args. (add `?usr=<your-user-name>&pwd=<room-pass>` to the url.)
|
||||
* Broadcasting room content using RTMP (to e.g. twitch or youtube...)
|
||||
* Bidirectional file transfer (if enabled)
|
||||
|
||||
### Why n.eko?
|
||||
|
||||
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 anyway.
|
||||
|
BIN
docs/_media/file-transfer.gif
Normal file
BIN
docs/_media/file-transfer.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 MiB |
8
docs/_media/icons/kde.svg
Normal file
8
docs/_media/icons/kde.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="128" viewBox="0 0 33.8667 33.8667" width="128" xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata/>
|
||||
<g transform="translate(0 -263.13)">
|
||||
<path d="m0 263.13h33.87v33.87h-33.87z" fill="#1d99f3"/>
|
||||
<path d="m18.8061 267.477-4.30242.41187v17.7156l4.25685-.64088v-7.55326l5.72205 8.37759 4.48586-1.41935-5.85934-8.05685 5.90521-7.59912-4.57729-1.05245-5.67649 7.59883zm-9.75254 4.31712c-.04858.005-.09551.0265-.13199.0632l-1.68863 1.68833c-.071.0712-.08437.18161-.03203.26782l1.97702 3.25614c-.35065.5895-.63169 1.22509-.83255 1.89559l-3.6295.75494c-.10098.0209-.17374.11039-.17374.21402v2.38772c0 .10098.06909.18844.16639.21196l3.52278.86107c.18786.77655.47897 1.51268.86372 2.18928l-2.03906 3.10944c-.05689.0869-.045.20138.0285.27458l1.68804 1.68834c.071.0708.18183.0847.26841.0326l3.19528-1.94057c.62765.36219 1.30454.64721 2.01995.8405l.74553 3.58451c.02098.10157.11076.17375.21373.17375h2.38802c.10039 0 .188-.0685.21196-.16699l.87812-3.59186c.73745-.19902 1.43492-.49565 2.07786-.8743l3.14883 2.06463c.08658.057.20109.0456.27458-.0279l1.68863-1.68834c.07143-.0714.08422-.18182.03174-.26781l-1.14947-1.89442-.37189.11759c-.05421.0171-.11333-.003-.14522-.0503 0 0-.73319-1.07332-1.68011-2.45915-1.13197 2.21544-3.43502 3.73297-6.09453 3.73297-3.77847 0-6.84183-3.06343-6.84183-6.84212 0-2.77974 1.65799-5.17032 4.03813-6.24122v-1.76506c-.43318.15154-.85196.33425-1.24972.55092-.00029-.00027-.00058-.001-.0017-.002l-3.22292-2.11384c-.04341-.0284-.09371-.0397-.14229-.0347z" fill="#fcfcfc" stroke-width=".265"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
docs/_media/intro.gif
Normal file
BIN
docs/_media/intro.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 MiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user